Preparing for 2.0 release.
diff --git a/README b/README
index d5a4a63..2f8e153 100644
--- a/README
+++ b/README
@@ -21,13 +21,13 @@
Sample Usage
------------
-Given a View view, the following code runs all accessibility checks and throws
-an exception if any errors are found:
+Given a root view, the following code runs all accessibility checks on all views in the
+hierarchy and and throws an exception if any errors are found:
-Set<AccessibilityCheck> checks = getAllChecksForPreset(VIEW_CHECKS);
+Set<AccessibilityViewHierarchyCheck> checks = getViewChecksForPreset(LATEST);
List<AccessibilityViewCheckResult> results = new LinkedList<AccessibilityViewCheckResult>();
-for (AccessibilityCheck check : checks) {
- results.addAll(((AccessibilityViewCheck) check).runCheckOnView(view));
+for (AccessibilityViewHierarchyCheck check : checks) {
+ results.addAll(check.runCheckOnViewHierarchy(rootView));
}
List<AccessibilityViewCheckResult> errors = AccessibilityCheckResultUtils.getResultsForType(
results, AccessibilityCheckResultType.ERROR);
diff --git a/pom.xml b/pom.xml
index 371823a..ef01e3b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,12 +4,48 @@
<groupId>com.google.android.apps.common.testing.accessibility.framework</groupId>
<artifactId>accessibility-test-framework</artifactId>
- <version>1.0-SNAPSHOT</version>
+ <version>2.0</version>
<packaging>jar</packaging>
<name>Accessibility Test Framework</name>
+ <description>Library used to test for common accessibility issues.</description>
<url>https://code.google.com/p/eyes-free/</url>
+ <licenses>
+ <license>
+ <name>Apache License, Version 2.0</name>
+ <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
+ <distribution>repo</distribution>
+ </license>
+ </licenses>
+
+ <developers>
+ <developer>
+ <name>Phil Weaver</name>
+ <email>[email protected]</email>
+ <organization>Google, Inc</organization>
+ <organizationUrl>http://www.google.com</organizationUrl>
+ </developer>
+ <developer>
+ <name>Casey Burkhardt</name>
+ <email>[email protected]</email>
+ <organization>Google, Inc</organization>
+ <organizationUrl>http://www.google.com</organizationUrl>
+ </developer>
+ <developer>
+ <name>Samuel Rush</name>
+ <email>[email protected]</email>
+ <organization>Google, Inc</organization>
+ <organizationUrl>http://www.google.com</organizationUrl>
+ </developer>
+ </developers>
+
+ <scm>
+ <connection>scm:svn:http://eyes-free.googlecode.com/svn/trunk/</connection>
+ <developerConnection>scm:svn:https://eyes-free.googlecode.com/svn/trunk/</developerConnection>
+ <url>https://code.google.com/p/eyes-free/source/browse</url>
+ </scm>
+
<dependencies>
<dependency>
<groupId>com.android.support</groupId>
@@ -48,6 +84,33 @@
</sdk>
</configuration>
</plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-source-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>attach-sources</id>
+ <goals>
+ <goal>jar</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-javadoc-plugin</artifactId>
+ <configuration>
+ <sourcepath>src</sourcepath>
+ </configuration>
+ <executions>
+ <execution>
+ <id>attach-javadocs</id>
+ <goals>
+ <goal>jar</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
</plugins>
</build>
-</project>
\ No newline at end of file
+</project>
diff --git a/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityCheck.java b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityCheck.java
index 7900aaf..50ce688 100644
--- a/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityCheck.java
+++ b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityCheck.java
@@ -22,9 +22,8 @@
* {@code AccessibilityInfoCheck}, the base class for checks that run against
* {@code AccessibilityNodeInfo}s.
*
- * Classes extending this one must implement {@code runCheck...} that return {@code List}s of
+ * <p>Classes extending this one must implement {@code runCheck...} that return {@code List}s of
* a subclass of {@code AccessibilityCheckResult}.
*/
public abstract class AccessibilityCheck {
-
}
diff --git a/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityCheckPreset.java b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityCheckPreset.java
index 2d86da8..ac7dbf0 100644
--- a/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityCheckPreset.java
+++ b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityCheckPreset.java
@@ -16,7 +16,6 @@
package com.google.android.apps.common.testing.accessibility.framework;
-import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
@@ -24,64 +23,121 @@
* Pre-sets for check configurations used with {@code getConfigForPreset}
*/
public enum AccessibilityCheckPreset {
+ /** The latest set of checks (in general the most comprehensive list */
+ LATEST,
+
+ /** The set of checks available in the 1.0 release of the framework */
+ VERSION_1_0_CHECKS,
+
/** Don't check anything */
NO_CHECKS,
- /** Check everything on a single View */
- VIEW_CHECKS,
-
- /** Check everything on a single AccessibilityNodeInfo */
- INFO_CHECKS,
-
- /** Check everything on all Views in a hierarchy */
- VIEW_HIERARCHY_CHECKS,
-
- /** Check everything on all AccessibilityNodeInfos in a hierarchy */
- INFO_HIERARCHY_CHECKS;
-
- private static final AccessibilityCheck[] ALL_ACCESSIBILITY_CHECKS = {
- new DuplicateSpeakableTextViewHierarchyCheck(),
- new EditableContentDescInfoCheck(),
- new EditableContentDescViewCheck(),
- new SpeakableTextPresentInfoCheck(),
- new SpeakableTextPresentViewCheck(),
- new TextContrastViewCheck(),
- new TouchTargetSizeViewCheck()
- };
+ /**
+ * Preset used occasionally to hold checks that are about to be part of {@code LATEST}.
+ * Includes all checks in {@code LATEST}.
+ */
+ PRERELEASE;
/**
* @param preset The preset of interest
- * @return An unmodifiable set of all checks with scopes that are covered by the specified scope
+ * @return A set of all checks for {@code View}s with scopes for the preset
*/
- public static Set<AccessibilityCheck> getAllChecksForPreset(AccessibilityCheckPreset preset) {
- Set<AccessibilityCheck> checks = new HashSet<AccessibilityCheck>();
- for (AccessibilityCheck check : ALL_ACCESSIBILITY_CHECKS) {
- switch (preset) {
- case VIEW_CHECKS:
- if (check instanceof AccessibilityViewCheck) {
- checks.add(check);
- }
- break;
- case VIEW_HIERARCHY_CHECKS:
- if (check instanceof AccessibilityViewHierarchyCheck) {
- checks.add(check);
- }
- break;
- case INFO_CHECKS:
- if (check instanceof AccessibilityInfoCheck) {
- checks.add(check);
- }
- break;
- case INFO_HIERARCHY_CHECKS:
- if (check instanceof AccessibilityInfoHierarchyCheck) {
- checks.add(check);
- }
- break;
- case NO_CHECKS:
- default:
- break;
- }
+ public static Set<AccessibilityViewHierarchyCheck>
+ getViewChecksForPreset(AccessibilityCheckPreset preset) {
+ Set<AccessibilityViewHierarchyCheck> checks = new HashSet<>();
+ if (preset == NO_CHECKS) {
+ return checks;
}
- return Collections.unmodifiableSet(checks);
+
+ /* Checks included in version 1.0 */
+ checks.add(new TouchTargetSizeViewCheck());
+ checks.add(new TextContrastViewCheck());
+ checks.add(new DuplicateSpeakableTextViewHierarchyCheck());
+ checks.add(new SpeakableTextPresentViewCheck());
+ checks.add(new EditableContentDescViewCheck());
+ if (preset == VERSION_1_0_CHECKS) {
+ return checks;
+ }
+
+ /* Checks added since last release */
+ checks.add(new ClickableSpanViewCheck());
+ checks.add(new RedundantContentDescViewCheck());
+ checks.add(new DuplicateClickableBoundsViewCheck());
+ if (preset == LATEST) {
+ return checks;
+ }
+
+ if (preset == PRERELEASE) {
+ return checks;
+ }
+ /*
+ * Throw an exception if we didn't handle a preset. This code should be unreachable, but it
+ * makes writing a test for unhandled presets trivial.
+ */
+ throw new IllegalArgumentException();
+ }
+
+ /**
+ * @param preset The preset of interest
+ * @return A set of all checks for {@code AccessibilityNodeInfo}s with scopes for the preset
+ */
+ public static Set<AccessibilityInfoHierarchyCheck>
+ getInfoChecksForPreset(AccessibilityCheckPreset preset) {
+ Set<AccessibilityInfoHierarchyCheck> checks = new HashSet<>();
+ if (preset == NO_CHECKS) {
+ return checks;
+ }
+
+ /* Checks included in version 1.0 */
+ checks.add(new EditableContentDescInfoCheck());
+ checks.add(new SpeakableTextPresentInfoCheck());
+ if (preset == VERSION_1_0_CHECKS) {
+ return checks;
+ }
+
+ /* Checks added since last release */
+ checks.add(new ClickableSpanInfoCheck());
+ checks.add(new TouchTargetSizeInfoCheck());
+ checks.add(new RedundantContentDescInfoCheck());
+ checks.add(new DuplicateClickableBoundsInfoCheck());
+ if (preset == LATEST) {
+ return checks;
+ }
+ if (preset == PRERELEASE) {
+ return checks;
+ }
+
+ /*
+ * Throw an exception if we didn't handle a preset. This code should be unreachable, but it
+ * makes writing a test for unhandled presets trivial.
+ */
+ throw new IllegalArgumentException();
+ }
+
+ /**
+ * @param preset The preset of interest
+ * @return A set of all checks for {@code AccessibilityNodeInfo}s with scopes for the preset
+ */
+ public static Set<AccessibilityEventCheck>
+ getEventChecksForPreset(AccessibilityCheckPreset preset) {
+ Set<AccessibilityEventCheck> checks = new HashSet<>();
+ if ((preset == NO_CHECKS) || (preset == VERSION_1_0_CHECKS)) {
+ return checks;
+ }
+
+ /* Checks added since last release */
+ checks.add(new AnnouncementEventCheck());
+ if (preset == LATEST) {
+ return checks;
+ }
+ if (preset == PRERELEASE) {
+ return checks;
+ }
+
+ /*
+ * Throw an exception if we didn't handle a preset. This code should be unreachable, but it
+ * makes writing a test for unhandled presets trivial.
+ */
+ throw new IllegalArgumentException();
}
}
diff --git a/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityCheckResult.java b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityCheckResult.java
index 5c5cf0c..8b1dcd3 100644
--- a/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityCheckResult.java
+++ b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityCheckResult.java
@@ -1,65 +1,74 @@
/*
* Copyright (C) 2014 Google Inc.
*
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
+ * 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
+ * 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;
+import com.google.android.apps.common.testing.accessibility.framework.proto.FrameworkProtos.AccessibilityCheckResultProto;
+
/**
- * The result of an accessibility check. The results are "interesting" in the sense
- * that they indicate some sort of accessibility issue. {@code AccessibilityCheck}s return lists
- * of classes that extend this one. There is no "passing" result; checks that return lists that
- * contain no {@code AccessibilityCheckResultType.ERROR}s have passed.
+ * The result of an accessibility check. The results are "interesting" in the sense that they
+ * indicate some sort of accessibility issue. {@code AccessibilityCheck}s return lists of classes
+ * that extend this one. There is no "passing" result; checks that return lists that contain no
+ * {@code AccessibilityCheckResultType.ERROR}s have passed.
+ * <p>
+ * NOTE: Some subtypes of this class retain copies of resources that should be explicitly recycled.
+ * Callers should use {@link #recycle()} to dispose of data in this object and release these
+ * resources.
*/
public abstract class AccessibilityCheckResult {
/**
- * Types of results
+ * Types of results. This must be kept consistent (other than UNKNOWN) with the ResultType enum in
+ * {@code AccessibilityCheckResultProto}.
*/
public enum AccessibilityCheckResultType {
/** Clearly an accessibility bug, for example no speakable text on a clicked button */
ERROR,
/**
- * Potentially an accessibility bug, for example finding another view with the same
- * speakable text as a clicked view
+ * Potentially an accessibility bug, for example finding another view with the same speakable
+ * text as a clicked view
*/
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.
+ * 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,
/**
* A signal that the check was not run at all (ex. because the API level was too low)
*/
- NOT_RUN
+ NOT_RUN,
+ /**
+ * A result that has been explicitly suppressed from throwing any Exceptions, used to allow for
+ * known issues.
+ */
+ SUPPRESSED
}
- protected final AccessibilityCheck check;
+ protected Class<? extends AccessibilityCheck> checkClass;
- protected final AccessibilityCheckResultType type;
+ protected AccessibilityCheckResultType type;
- protected final CharSequence message;
+ protected CharSequence message;
/**
- * @param check The check that generated the error
+ * @param checkClass The class of the check that generated the error
* @param type The type of the result
* @param message A human-readable message explaining the error
*/
- protected AccessibilityCheckResult(AccessibilityCheck check, AccessibilityCheckResultType type,
- CharSequence message) {
- this.check = check;
+ public AccessibilityCheckResult(Class<? extends AccessibilityCheck> checkClass,
+ AccessibilityCheckResultType type, CharSequence message) {
+ this.checkClass = checkClass;
this.type = type;
this.message = message;
}
@@ -67,8 +76,8 @@
/**
* @return The check that generated the result.
*/
- public AccessibilityCheck getSourceCheck() {
- return check;
+ public Class<? extends AccessibilityCheck> getSourceCheckClass() {
+ return checkClass;
}
/**
@@ -79,9 +88,43 @@
}
/**
+ * @param type The type of the result.
+ */
+ /* package */ void setType(AccessibilityCheckResultType type) {
+ this.type = type;
+ }
+
+ /**
* @return A human-readable message explaining the result.
*/
public CharSequence getMessage() {
return message;
}
+
+ /**
+ * Recycles all resources held by this object.
+ */
+ public void recycle() {
+ checkClass = null;
+ type = null;
+ message = null;
+ }
+
+ /**
+ * Returns a populated {@link AccessibilityCheckResultProto}
+ */
+ public AccessibilityCheckResultProto toProto() {
+ AccessibilityCheckResultProto.Builder builder = AccessibilityCheckResultProto.newBuilder();
+ if (type != null) {
+ // enum in this class and proto are consistent, one can resolve the string name in the other
+ builder.setResultType(AccessibilityCheckResultProto.ResultType.valueOf(type.name()));
+ }
+ if (message != null) {
+ builder.setMsg(message.toString());
+ }
+ if (checkClass != null) {
+ builder.setSourceCheckClass(checkClass.getName());
+ }
+ return builder.build();
+ }
}
diff --git a/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityCheckResultUtils.java b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityCheckResultUtils.java
index 432209d..24a10f2 100644
--- a/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityCheckResultUtils.java
+++ b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityCheckResultUtils.java
@@ -21,20 +21,27 @@
import android.view.View;
import android.view.accessibility.AccessibilityNodeInfo;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+
import java.util.ArrayList;
import java.util.List;
/**
* Utility class for dealing with {@code AccessibilityCheckResult}s
*/
-public class AccessibilityCheckResultUtils {
+public final class AccessibilityCheckResultUtils {
- public AccessibilityCheckResultUtils() {
+ private AccessibilityCheckResultUtils() {
}
/**
- * Takes a list of {@code AccessibilityCheckResult}s and returns a list with only results
- * obtained from the given {@code AccessibilityCheck}.
+ * Takes a list of {@code AccessibilityCheckResult}s and returns a list with only results obtained
+ * from the given {@code AccessibilityCheck}.
+ * <p>
+ * NOTE: This method explicitly does not take subtypes of {@code checkClass} into account when
+ * filtering results.
*
* @param results a list of {@code AccessibilityCheckResult}s
* @param checkClass the {@code Class} of the {@code AccessibilityCheck} to get results for
@@ -45,7 +52,7 @@
Iterable<T> results, Class<? extends AccessibilityCheck> checkClass) {
List<T> resultsForCheck = new ArrayList<T>();
for (T result : results) {
- if (checkClass.isInstance(result.getSourceCheck())) {
+ if (checkClass.equals(result.getSourceCheckClass())) {
resultsForCheck.add(result);
}
}
@@ -112,4 +119,127 @@
}
return resultsForInfo;
}
+
+ /**
+ * Returns a {@link Matcher} for an {@link AccessibilityCheckResult} whose result type matches the
+ * given matcher for {@link AccessibilityCheckResultType}.
+ *
+ * @param typeMatcher a {@code Matcher} for an {@code AccessibilityCheckResultType}
+ * @return a {@code Matcher} for an {@code AccessibilityCheckResult}
+ */
+ public static Matcher<AccessibilityCheckResult> matchesTypes(
+ final Matcher<? super AccessibilityCheckResultType> typeMatcher) {
+ if (typeMatcher == null) {
+ return null;
+ }
+ return new TypeSafeMemberMatcher<AccessibilityCheckResult>("result type", typeMatcher) {
+ @Override
+ public boolean matchesSafely(AccessibilityCheckResult result) {
+ return typeMatcher.matches(result.getType());
+ }
+ };
+ }
+
+ /**
+ * Returns a {@link Matcher} for an {@link AccessibilityCheckResult} whose source check class has
+ * a simple name that matches the given matcher for a {@code String}.
+ *
+ * @param classNameMatcher a {@code Matcher} for a {@code String}
+ * @return a {@code Matcher} for an {@code AccessibilityCheckResult}
+ */
+ public static Matcher<AccessibilityCheckResult> matchesCheckNames(
+ final Matcher<? super String> classNameMatcher) {
+ if (classNameMatcher == null) {
+ return null;
+ }
+ return new TypeSafeMemberMatcher<AccessibilityCheckResult>("source check name",
+ classNameMatcher) {
+ @Override
+ public boolean matchesSafely(AccessibilityCheckResult result) {
+ return classNameMatcher.matches(result.getSourceCheckClass().getSimpleName());
+ }
+ };
+ }
+
+ /**
+ * Returns a {@link Matcher} for an {@link AccessibilityViewCheckResult} whose view
+ * matches the given matcher for a {@link View}.
+ *
+ * @param viewMatcher a {@code Matcher} for a {@code View}
+ * @return a {@code Matcher} for an {@code AccessibilityCheckResult}
+ */
+ public static Matcher<AccessibilityViewCheckResult> matchesViews(
+ final Matcher<? super View> viewMatcher) {
+ if (viewMatcher == null) {
+ return null;
+ }
+ return new TypeSafeMemberMatcher<AccessibilityViewCheckResult>("View", viewMatcher) {
+ @Override
+ public boolean matchesSafely(AccessibilityViewCheckResult result) {
+ return viewMatcher.matches(result.getView());
+ }
+ };
+ }
+
+ /**
+ * Returns a {@link Matcher} for an {@link AccessibilityNodeInfo} whose
+ * {@link AccessibilityNodeInfo} matches the given matcher.
+ *
+ * @param infoMatcher a {@code Matcher} for a {@code AccessibilityNodeInfo}
+ * @return a {@code Matcher} for an {@code AccessibilityCheckResult}
+ */
+ public static Matcher<AccessibilityInfoCheckResult> matchesInfos(
+ final Matcher<? super AccessibilityNodeInfo> infoMatcher) {
+ if (infoMatcher == null) {
+ return null;
+ }
+ return new TypeSafeMemberMatcher<AccessibilityInfoCheckResult>("AccessibilityNodeInfo",
+ infoMatcher) {
+ @Override
+ public boolean matchesSafely(AccessibilityInfoCheckResult result) {
+ return infoMatcher.matches(result.getInfo());
+ }
+ };
+ }
+
+ /**
+ * Change the result type to {@code SUPPRESSED} for all results in the given list that match the
+ * given matcher.
+ *
+ * @param results a list of {@code AccessibilityCheckResult}s to be matched against
+ * @param matcher a Matcher that determines whether a given {@code AccessibilityCheckResult}
+ * should be suppressed
+ */
+ public static <T extends AccessibilityCheckResult> void suppressMatchingResults(List<T> results,
+ Matcher<? super T> matcher) {
+ modifyResultType(results, matcher, AccessibilityCheckResultType.SUPPRESSED);
+ }
+
+ private static <T extends AccessibilityCheckResult> void modifyResultType(List<T> results,
+ Matcher<? super T> matcher, AccessibilityCheckResultType newType) {
+ if (results == null || matcher == null) {
+ return;
+ }
+ for (T result : results) {
+ if (matcher.matches(result)) {
+ result.setType(newType);
+ }
+ }
+ }
+
+ private abstract static class TypeSafeMemberMatcher<T> extends TypeSafeMatcher<T> {
+ private static final String DESCRIPTION_FORMAT_STRING = "with %s: ";
+ private String memberDescription;
+ private Matcher<?> matcher;
+ public TypeSafeMemberMatcher(String member, Matcher<?> matcher) {
+ memberDescription = String.format(DESCRIPTION_FORMAT_STRING, member);
+ this.matcher = matcher;
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendText(memberDescription);
+ matcher.describeTo(description);
+ }
+ }
}
diff --git a/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityCheckUtils.java b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityCheckUtils.java
index 9b4e10d..9d828ca 100644
--- a/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityCheckUtils.java
+++ b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityCheckUtils.java
@@ -1,33 +1,32 @@
/*
* Copyright (C) 2014 Google Inc.
*
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
+ * 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
+ * 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;
+import android.os.Build;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.CompoundButton;
import android.widget.TextView;
import com.googlecode.eyesfree.utils.AccessibilityNodeInfoUtils;
+import com.googlecode.eyesfree.utils.StringBuilderUtils;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Set;
@@ -37,54 +36,14 @@
* <p>All methods that take {@link View} or {@link AccessibilityNodeInfo} objects as arguments
* require them to be fully initialized and in a valid hierarchy. Neither a newly constructed
* {@link View} nor an object returned by {@code View.createAccessibilityNodeInfo()} have the
- * required properties because calls like {@code View.getRootView()} and {@code
- * AccessibilityNodeInfo.getParent} do not return reasonable values unless the objects are part
- * of a valid hierarchy.
+ * required properties because calls like {@code View.getRootView()} and
+ * {@code AccessibilityNodeInfo.getParent} do not return reasonable values unless the objects are
+ * part of a valid hierarchy.
*/
-public final class AccessibilityCheckUtils {
+final class AccessibilityCheckUtils {
private AccessibilityCheckUtils() {}
- private static Method isImportantForAccessibilityMethod;
-
- static {
- try {
- isImportantForAccessibilityMethod = View.class.getMethod("isImportantForAccessibility");
- } catch (NoSuchMethodException e) {
- isImportantForAccessibilityMethod = null;
- } catch (SecurityException e) {
- isImportantForAccessibilityMethod = null;
- }
- }
- /**
- * @param rootView The root of a View hierarchy
- * @return A Set containing the root view and all views below it in the hierarchy
- */
- public static Set<View> getAllViewsInHierarchy(View rootView) {
- Set<View> allViews = new HashSet<View>();
- allViews.add(rootView);
- addAllChildrenToSet(rootView, allViews);
- return allViews;
- }
-
- /**
- * Add all children in the view tree rooted at rootView to a set
- * @param rootView The root of the view tree desired
- * @param theSet The set to add views to
- */
- private static void addAllChildrenToSet(View rootView, Set<View> theSet) {
- if (!(rootView instanceof ViewGroup)) {
- return;
- }
-
- ViewGroup rootViewGroup = (ViewGroup) rootView;
- for (int i = 0; i < rootViewGroup.getChildCount(); ++i) {
- View nextView = rootViewGroup.getChildAt(i);
- theSet.add(nextView);
- addAllChildrenToSet(nextView, theSet);
- }
- }
-
/**
* Retrieve text for a node, which may include text from children of the node. This text is an
* approximation of, but not always identical to, what TalkBack would speak for the node. One
@@ -92,13 +51,45 @@
*
* @param info The info whose text should be returned.
*
- * @return Speakable text derived from the info and its children. Returns an empty string if
- * there is no such text, and {@code null} if {@code info == null}.
+ * @return Speakable text derived from the info and its children. Returns an empty string if there
+ * is no such text, and {@code null} if {@code info == null}.
*/
- public static CharSequence getSpeakableTextForInfo(AccessibilityNodeInfo info) {
+ static CharSequence getSpeakableTextForInfo(AccessibilityNodeInfo info) {
if (info == null) {
return null;
}
+
+ /* getLabeledBy for API 17+ */
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN) {
+
+ AccessibilityNodeInfo labeledBy = info.getLabeledBy();
+ if (labeledBy != null) {
+ /* There could be a chain of labeledBy. Make sure it isn't a loop */
+ Set<AccessibilityNodeInfo> infosVisited = new HashSet<>();
+ infosVisited.add(info);
+ infosVisited.add(labeledBy);
+ AccessibilityNodeInfo endOfLabeledByChain = labeledBy.getLabeledBy();
+ while (endOfLabeledByChain != null) {
+ if (infosVisited.contains(endOfLabeledByChain)) {
+ infosVisited.remove(info);
+ for (AccessibilityNodeInfo infoVisited : infosVisited) {
+ infoVisited.recycle();
+ }
+ return null;
+ }
+ infosVisited.add(endOfLabeledByChain);
+ labeledBy = endOfLabeledByChain;
+ endOfLabeledByChain = labeledBy.getLabeledBy();
+ }
+ CharSequence labelText = getSpeakableTextForInfo(labeledBy);
+ infosVisited.remove(info);
+ for (AccessibilityNodeInfo infoVisited : infosVisited) {
+ infoVisited.recycle();
+ }
+ return labelText;
+ }
+ }
+
AccessibilityNodeInfoCompat compat = new AccessibilityNodeInfoCompat(info);
// TODO(caseburkhardt) Pull in TalkBack's actual logic
@@ -110,8 +101,8 @@
AccessibilityNodeInfoCompat child = compat.getChild(i);
if (AccessibilityNodeInfoUtils.isVisibleOrLegacy(child)
&& !AccessibilityNodeInfoUtils.isActionableForAccessibility(child)) {
- returnStringBuilder
- .append(getSpeakableTextForInfo((AccessibilityNodeInfo) child.getInfo()));
+ returnStringBuilder.append(
+ getSpeakableTextForInfo((AccessibilityNodeInfo) child.getInfo()));
}
}
}
@@ -131,74 +122,55 @@
* @param view The {@link View} whose text should be returned.
*
* @return Speakable text derived from the {@link View} and its children. Returns an empty string
- * if there is no such text, and {@code null} if {@code view == null}.
+ * if there is no such text, and {@code null} if {@code view == null}.
*/
- public static CharSequence getSpeakableTextForView(View view) {
+ static CharSequence getSpeakableTextForView(View view) {
if (view == null) {
return null;
}
- SpannableStringBuilder returnStringBuilder = new SpannableStringBuilder("");
-
- boolean isImportantForAccessibility;
- /*
- * TODO(pweaver): Call view.isImportantForAccessibility() if (Build.VERSION.SDK_INT). Need
- * ViewCompat to support method
- */
- if (isImportantForAccessibilityMethod != null) {
- try {
- isImportantForAccessibility = (Boolean) isImportantForAccessibilityMethod.invoke(view);
- } catch (IllegalAccessException e) {
- e.printStackTrace();
- isImportantForAccessibility = true;
- } catch (IllegalArgumentException e) {
- e.printStackTrace();
- isImportantForAccessibility = true;
- } catch (InvocationTargetException e) {
- e.printStackTrace();
- isImportantForAccessibility = true;
- }
- } else {
- isImportantForAccessibility = true;
+ View labelForThisView = ViewAccessibilityUtils.getLabelForView(view);
+ if (labelForThisView != null) {
+ return getSpeakableTextForView(labelForThisView);
}
- /* Content descriptions override everything else */
- if (isImportantForAccessibility) {
- if (!TextUtils.isEmpty(view.getContentDescription())) {
- return view.getContentDescription();
+ SpannableStringBuilder returnStringBuilder = new SpannableStringBuilder("");
+
+ // Accessibility importance is considered only on Jelly Bean and above
+ if ((Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN)
+ || ViewAccessibilityUtils.isImportantForAccessibility(view)) {
+ if (!TextUtils.isEmpty(view.getContentDescription())) {
+ // contentDescription always wins out over other properties
+ return view.getContentDescription();
+ }
+ if (view instanceof TextView) {
+ if (!TextUtils.isEmpty(((TextView) view).getText())) {
+ returnStringBuilder.append(((TextView) view).getText());
+ } else if (!TextUtils.isEmpty(((TextView) view).getHint())) {
+ returnStringBuilder.append(((TextView) view).getHint());
}
- if (view instanceof TextView) {
- if (!TextUtils.isEmpty(((TextView) view).getText())) {
- returnStringBuilder.append(((TextView) view).getText());
- } else if (!TextUtils.isEmpty(((TextView) view).getHint())) {
- returnStringBuilder.append(((TextView) view).getHint());
- }
- }
+ }
}
if (view instanceof ViewGroup) {
ViewGroup group = (ViewGroup) view;
+ // TODO(sjrush): Only evaluate child views if they're importantForAccessibility.
for (int i = 0; i < group.getChildCount(); ++i) {
View childView = group.getChildAt(i);
if ((childView.getVisibility() == View.VISIBLE)
- && !isActionableForAccessibility(childView)) {
+ && !ViewAccessibilityUtils.isActionableForAccessibility(childView)) {
returnStringBuilder.append(getSpeakableTextForView(childView));
}
}
}
- return returnStringBuilder;
- }
- /**
- * @param view
- * @return {@code true} if view's {@link AccessibilityNodeInfo} would be deemed actionable for
- * accessibility.
- */
- private static boolean isActionableForAccessibility(View view) {
- if (view == null) {
- return false;
+ if (view instanceof CompoundButton) {
+ if (((CompoundButton) view).isChecked()) {
+ StringBuilderUtils.appendWithSeparator(returnStringBuilder, "Checked");
+ } else {
+ StringBuilderUtils.appendWithSeparator(returnStringBuilder, "Not checked");
+ }
}
-
- return ((view.isClickable() || view.isLongClickable()) || view.isFocusable());
+ return returnStringBuilder;
}
}
diff --git a/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityInfoCheck.java b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityInfoCheck.java
index 6e0e1d6..379aa58 100644
--- a/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityInfoCheck.java
+++ b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityInfoCheck.java
@@ -16,20 +16,37 @@
package com.google.android.apps.common.testing.accessibility.framework;
+import android.content.Context;
+import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.view.accessibility.AccessibilityNodeInfo;
+import java.util.ArrayList;
import java.util.List;
/**
* Base class to check the accessibility of {@code AccessibilityNodeInfo}s.
*/
-public abstract class AccessibilityInfoCheck extends AccessibilityCheck {
+public abstract class AccessibilityInfoCheck extends AccessibilityInfoHierarchyCheck {
+ @Override
+ public List<AccessibilityInfoCheckResult> runCheckOnInfoHierarchy(
+ AccessibilityNodeInfo root, Context context) {
+ List<AccessibilityInfoCheckResult> results = new ArrayList<>();
+ List<AccessibilityNodeInfoCompat> compatInfos = getAllInfoCompatsInHierarchy(context, root);
+ for (AccessibilityNodeInfoCompat compatInfo : compatInfos) {
+ AccessibilityNodeInfo info = (AccessibilityNodeInfo) compatInfo.getInfo();
+ results.addAll(runCheckOnInfo(info, context));
+ }
+ return results;
+ }
+
/**
* Run the check on the view.
* @param info The node info to check. The info should be fully initialized and part of a valid
* hierarchy so all of its methods can be called.
+ * @param context The context of the service.
* @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<AccessibilityInfoCheckResult> runCheckOnInfo(AccessibilityNodeInfo info);
+ public abstract List<AccessibilityInfoCheckResult> runCheckOnInfo(AccessibilityNodeInfo info,
+ Context context);
}
diff --git a/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityInfoCheckResult.java b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityInfoCheckResult.java
index f297e27..382573a 100644
--- a/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityInfoCheckResult.java
+++ b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityInfoCheckResult.java
@@ -1,47 +1,132 @@
/*
* Copyright (C) 2014 Google Inc.
*
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
+ * 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
+ * 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;
+import android.annotation.TargetApi;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.Log;
import android.view.accessibility.AccessibilityNodeInfo;
+import com.googlecode.eyesfree.utils.LogUtils;
+
/**
* Result generated when an accessibility check runs on a {@code AccessibilityNodeInfo}.
*/
-public class AccessibilityInfoCheckResult extends AccessibilityCheckResult {
+@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
+public final class AccessibilityInfoCheckResult extends AccessibilityCheckResult implements
+ Parcelable {
private AccessibilityNodeInfo info;
/**
- * @param check The check that generated the error
+ * @param checkClass The check that generated the error
* @param type The type of the result
* @param message A human-readable message explaining the error
* @param info The info that was responsible for generating the error
*/
- AccessibilityInfoCheckResult(AccessibilityCheck check, AccessibilityCheckResultType type,
- CharSequence message, AccessibilityNodeInfo info) {
- super(check, type, message);
- this.info = info;
+ public AccessibilityInfoCheckResult(Class<? extends AccessibilityCheck> checkClass,
+ AccessibilityCheckResultType type, CharSequence message, AccessibilityNodeInfo info) {
+ super(checkClass, type, message);
+ if (info != null) {
+ this.info = AccessibilityNodeInfo.obtain(info);
+ }
+ }
+
+ private AccessibilityInfoCheckResult(Parcel in) {
+ super(null, null, null);
+ readFromParcel(in);
}
/**
* @return The info to which the result applies.
*/
- AccessibilityNodeInfo getInfo() {
+ public AccessibilityNodeInfo getInfo() {
return info;
}
+ @Override
+ public void recycle() {
+ super.recycle();
+
+ if (info != null) {
+ info.recycle();
+ info = null;
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString((checkClass != null) ? checkClass.getName() : "");
+ dest.writeInt((type != null) ? type.ordinal() : -1);
+ TextUtils.writeToParcel(message, dest, flags);
+ // Info requires a presence flag
+ if (info != null) {
+ dest.writeInt(1);
+ info.writeToParcel(dest, flags);
+ } else {
+ dest.writeInt(0);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private void readFromParcel(Parcel in) {
+ // Check class (unchecked cast checked by isAssignableFrom)
+ checkClass = null;
+ String checkClassName = in.readString();
+ if (!("".equals(checkClassName))) {
+ try {
+ Class<?> uncheckedClass = Class.forName(checkClassName);
+ if (AccessibilityCheck.class.isAssignableFrom(uncheckedClass)) {
+ checkClass = (Class<? extends AccessibilityCheck>) uncheckedClass;
+ }
+ } catch (ClassNotFoundException e) {
+ // If the reference can't be resolved by our class loader, remain null.
+ LogUtils.log(this, Log.WARN, "Attempt to obtain unknown class %1$s", checkClassName);
+ }
+ }
+
+ // Type
+ final int type = in.readInt();
+ this.type = (type != -1) ? AccessibilityCheckResultType.values()[type] : null;
+
+ // Message
+ this.message = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
+
+ // Info
+ this.info = (in.readInt() == 1) ? AccessibilityNodeInfo.CREATOR.createFromParcel(in) : null;
+
+ }
+
+ public static final Parcelable.Creator<AccessibilityInfoCheckResult> CREATOR =
+ new Parcelable.Creator<AccessibilityInfoCheckResult>() {
+ @Override
+ public AccessibilityInfoCheckResult createFromParcel(Parcel in) {
+ return new AccessibilityInfoCheckResult(in);
+ }
+
+ @Override
+ public AccessibilityInfoCheckResult[] newArray(int size) {
+ return new AccessibilityInfoCheckResult[size];
+ }
+ };
}
diff --git a/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityInfoHierarchyCheck.java b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityInfoHierarchyCheck.java
index 83eeb26..8e85858 100644
--- a/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityInfoHierarchyCheck.java
+++ b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityInfoHierarchyCheck.java
@@ -16,24 +16,44 @@
package com.google.android.apps.common.testing.accessibility.framework;
+import android.content.Context;
+import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.view.accessibility.AccessibilityNodeInfo;
+import com.googlecode.eyesfree.utils.AccessibilityNodeInfoUtils;
+import com.googlecode.eyesfree.utils.NodeFilter;
+
import java.util.List;
/**
* Base class to check the accessibility of all {@code Info}s in a hierarchy.
*/
public abstract class AccessibilityInfoHierarchyCheck extends AccessibilityCheck {
+ private static final NodeFilter WIDE_OPEN_FILTER = new NodeFilter() {
+ @Override
+ public boolean accept(Context context, AccessibilityNodeInfoCompat node) {
+ return true;
+ }
+ };
+
public AccessibilityInfoHierarchyCheck() {
}
/**
* Run the check on the info.
* @param root The root of the AccessibilityNodeInfo hierarchy to check.
+ * @param context The context of the service.
* @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<AccessibilityInfoCheckResult> runCheckOnInfoHierarchy(
- AccessibilityNodeInfo root);
+ AccessibilityNodeInfo root, Context context);
+
+ static List<AccessibilityNodeInfoCompat> getAllInfoCompatsInHierarchy(Context context,
+ AccessibilityNodeInfo root) {
+ return AccessibilityNodeInfoUtils.searchAllFromBfs(context,
+ new AccessibilityNodeInfoCompat(root),
+ AccessibilityInfoHierarchyCheck.WIDE_OPEN_FILTER);
+ }
}
diff --git a/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityViewCheck.java b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityViewCheck.java
index 0865ab8..f156975 100644
--- a/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityViewCheck.java
+++ b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityViewCheck.java
@@ -18,15 +18,30 @@
import android.view.View;
+import java.util.ArrayList;
import java.util.List;
+import java.util.Set;
/**
- * Base class to check the accessibility of {@code View}s.
+ * Base class for checking a single View.
+ * Use hierarchy checks instead. All single-view checks are being rewritten to be
+ * hierarchy checks instead.
*/
-public abstract class AccessibilityViewCheck extends AccessibilityCheck {
+@Deprecated
+public abstract class AccessibilityViewCheck extends AccessibilityViewHierarchyCheck {
public AccessibilityViewCheck() {
}
+ @Override
+ public List<AccessibilityViewCheckResult> runCheckOnViewHierarchy(View root) {
+ List<AccessibilityViewCheckResult> results = new ArrayList<>();
+ Set<View> viewsToCheck = ViewAccessibilityUtils.getAllViewsInHierarchy(root);
+ for (View view : viewsToCheck) {
+ results.addAll(runCheckOnView(view));
+ }
+ return results;
+ }
+
/**
* Run the check on the view.
* @param view The view to check. The view should be fully initialized and part of a valid view
@@ -35,5 +50,4 @@
* empty if the check passes without incident.
*/
public abstract List<AccessibilityViewCheckResult> runCheckOnView(View view);
-
}
diff --git a/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityViewCheckResult.java b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityViewCheckResult.java
index 30962b3..1f0dbd4 100644
--- a/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityViewCheckResult.java
+++ b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityViewCheckResult.java
@@ -1,16 +1,14 @@
/*
* Copyright (C) 2014 Google Inc.
*
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
+ * 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
+ * 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.
*/
@@ -26,14 +24,14 @@
private View view;
/**
- * @param check The check that generated the error
+ * @param checkClass The check that generated the error
* @param type The type of the result
* @param message A human-readable message explaining the error
* @param view The view that was responsible for generating the error
*/
- AccessibilityViewCheckResult(AccessibilityCheck check, AccessibilityCheckResultType type,
- CharSequence message, View view) {
- super(check, type, message);
+ public AccessibilityViewCheckResult(Class<? extends AccessibilityCheck> checkClass,
+ AccessibilityCheckResultType type, CharSequence message, View view) {
+ super(checkClass, type, message);
this.view = view;
}
@@ -44,4 +42,9 @@
return view;
}
+ @Override
+ public void recycle() {
+ super.recycle();
+ view = null;
+ }
}
diff --git a/src/main/java/com/google/android/apps/common/testing/accessibility/framework/DuplicateSpeakableTextViewHierarchyCheck.java b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/DuplicateSpeakableTextViewHierarchyCheck.java
index 1f42e3b..b934a2e 100644
--- a/src/main/java/com/google/android/apps/common/testing/accessibility/framework/DuplicateSpeakableTextViewHierarchyCheck.java
+++ b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/DuplicateSpeakableTextViewHierarchyCheck.java
@@ -1,16 +1,14 @@
/*
* Copyright (C) 2014 Google Inc.
*
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
+ * 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
+ * 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.
*/
@@ -18,20 +16,22 @@
import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType;
+import android.os.Build;
import android.text.TextUtils;
import android.view.View;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Set;
/**
- * If two Views in a hierarchy have the same speakable text, that could be confusing for users.
- * Two Views with the same text, and at least one of them is clickable we warn in that situation.
- * If we find two non-clickable Views with the same speakable text, we report that fact as info.
- * If no Views in the hierarchy have any speakable text, we report that the test was not run.
+ * If two Views in a hierarchy have the same speakable text, that could be confusing for users. Two
+ * Views with the same text, and at least one of them is clickable we warn in that situation. If we
+ * find two non-clickable Views with the same speakable text, we report that fact as info. If no
+ * Views in the hierarchy have any speakable text, we report that the test was not run.
*/
public class DuplicateSpeakableTextViewHierarchyCheck extends AccessibilityViewHierarchyCheck {
@@ -39,8 +39,15 @@
public List<AccessibilityViewCheckResult> runCheckOnViewHierarchy(View root) {
List<AccessibilityViewCheckResult> results = new ArrayList<AccessibilityViewCheckResult>();
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
+ results.add(new AccessibilityViewCheckResult(this.getClass(),
+ AccessibilityCheckResultType.NOT_RUN, "This check only runs on Andorid 4.1 and above.",
+ root));
+ return results;
+ }
+
/* Find all text and the views that have that text */
- Set<View> allViews = AccessibilityCheckUtils.getAllViewsInHierarchy(root);
+ Set<View> allViews = ViewAccessibilityUtils.getAllViewsInHierarchy(root);
Map<String, List<View>> textToViewMap = getSpeakableTextToViewMap(allViews);
/* Deal with any duplicated text */
@@ -62,36 +69,40 @@
if (clickableViews.size() > 0) {
/* Display warning */
- results.add(new AccessibilityViewCheckResult(this, AccessibilityCheckResultType.WARNING,
- String.format("Clickable view's speakable text: \"%s\" is identical to that of %d "
+ results.add(new AccessibilityViewCheckResult(this.getClass(),
+ AccessibilityCheckResultType.WARNING, String.format(Locale.US,
+ "Clickable view's speakable text: \"%s\" is identical to that of %d "
+ "other view(s)", speakableText, nonClickableViews.size()),
- clickableViews.get(0)));
+ clickableViews.get(0)));
clickableViews.remove(0);
} else {
/* Only duplication is on non-clickable views */
- results.add(new AccessibilityViewCheckResult(this, AccessibilityCheckResultType.INFO,
- String.format("Non-clickable view's speakable text: \"%s\" is identical to that of %d "
+ results.add(new AccessibilityViewCheckResult(this.getClass(),
+ AccessibilityCheckResultType.INFO, String.format(Locale.US,
+ "Non-clickable view's speakable text: \"%s\" is identical to that of %d "
+ "other non-clickable view(s)", speakableText, nonClickableViews.size() - 1),
- nonClickableViews.get(0)));
+ nonClickableViews.get(0)));
nonClickableViews.remove(0);
}
/* Add infos to help track down the duplication */
for (View clickableView : clickableViews) {
- results.add(new AccessibilityViewCheckResult(this, AccessibilityCheckResultType.INFO,
+ results.add(new AccessibilityViewCheckResult(this.getClass(),
+ AccessibilityCheckResultType.INFO,
String.format(" Clickable View has speakable text: \"%s\".", speakableText),
clickableView));
}
for (View clickableView : nonClickableViews) {
- results.add(new AccessibilityViewCheckResult(this, AccessibilityCheckResultType.INFO,
+ results.add(new AccessibilityViewCheckResult(this.getClass(),
+ AccessibilityCheckResultType.INFO,
String.format(" Non-clickable View has speakable text: \"%s\".", speakableText),
clickableView));
}
}
if (textToViewMap.size() == 0) {
- results.add(new AccessibilityViewCheckResult(this, AccessibilityCheckResultType.NOT_RUN,
- "No Views in hierarchy have speakable text", root));
+ results.add(new AccessibilityViewCheckResult(this.getClass(),
+ AccessibilityCheckResultType.NOT_RUN, "No Views in hierarchy have speakable text", root));
return results;
}
@@ -106,8 +117,8 @@
Map<String, List<View>> textToViewMap = new HashMap<String, List<View>>();
for (View view : allViews) {
- String speakableText = AccessibilityCheckUtils.getSpeakableTextForView(view)
- .toString().trim();
+ String speakableText =
+ AccessibilityCheckUtils.getSpeakableTextForView(view).toString().trim();
if (TextUtils.isEmpty(speakableText)) {
continue;
}
diff --git a/src/main/java/com/google/android/apps/common/testing/accessibility/framework/EditableContentDescInfoCheck.java b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/EditableContentDescInfoCheck.java
index 076b96a..6515ebf 100644
--- a/src/main/java/com/google/android/apps/common/testing/accessibility/framework/EditableContentDescInfoCheck.java
+++ b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/EditableContentDescInfoCheck.java
@@ -1,16 +1,14 @@
/*
* Copyright (C) 2014 Google Inc.
*
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
+ * 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
+ * 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.
*/
@@ -18,6 +16,7 @@
import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType;
+import android.content.Context;
import android.os.Build;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.text.TextUtils;
@@ -35,19 +34,21 @@
public class EditableContentDescInfoCheck extends AccessibilityInfoCheck {
@Override
- public List<AccessibilityInfoCheckResult> runCheckOnInfo(AccessibilityNodeInfo info) {
+ public List<AccessibilityInfoCheckResult> runCheckOnInfo(AccessibilityNodeInfo info,
+ Context context) {
List<AccessibilityInfoCheckResult> results = new ArrayList<AccessibilityInfoCheckResult>(1);
AccessibilityNodeInfoCompat compatInfo = new AccessibilityNodeInfoCompat(info);
- if (getAndroidVersion() >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
if (info.isEditable()) {
if (!TextUtils.isEmpty(info.getContentDescription())) {
- results.add(new AccessibilityInfoCheckResult(this, AccessibilityCheckResultType.ERROR,
- "Editable view should not have a contentDescription", info));
+ results.add(new AccessibilityInfoCheckResult(this.getClass(),
+ AccessibilityCheckResultType.ERROR,
+ "Editable view should not have a contentDescription", info));
}
} else {
- results.add(new AccessibilityInfoCheckResult(this, AccessibilityCheckResultType.NOT_RUN,
- "Associated view must be editable", info));
+ results.add(new AccessibilityInfoCheckResult(this.getClass(),
+ AccessibilityCheckResultType.NOT_RUN, "Associated view must be editable", info));
}
} else {
// Earlier API versions don't allow us to filter based on editable state, so we'll fall back
@@ -57,24 +58,15 @@
// defined within other packages.
if (AccessibilityNodeInfoUtils.nodeMatchesAnyClassByType(null, compatInfo, EditText.class)) {
if (!TextUtils.isEmpty(compatInfo.getContentDescription())) {
- results.add(new AccessibilityInfoCheckResult(this, AccessibilityCheckResultType.ERROR,
- "EditText should not have a contentDescription", info));
+ results.add(new AccessibilityInfoCheckResult(this.getClass(),
+ AccessibilityCheckResultType.ERROR, "EditText should not have a contentDescription",
+ info));
}
} else {
- results.add(new AccessibilityInfoCheckResult(this, AccessibilityCheckResultType.NOT_RUN,
- "Associated view must be an EditText", info));
+ results.add(new AccessibilityInfoCheckResult(this.getClass(),
+ AccessibilityCheckResultType.NOT_RUN, "Associated view must be an EditText", info));
}
}
return results;
}
-
- /**
- * @return The Android Build.VERSION.SDK_INT value for the current runtime.
- *
- * NOTE: This wrapper exists only for testing.
- */
- /*package*/ int getAndroidVersion() {
- // TODO(caseyburkhardt): Find a more robust way to support testing of this.
- return Build.VERSION.SDK_INT;
- }
}
diff --git a/src/main/java/com/google/android/apps/common/testing/accessibility/framework/EditableContentDescViewCheck.java b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/EditableContentDescViewCheck.java
index ead3f6d..342f917 100644
--- a/src/main/java/com/google/android/apps/common/testing/accessibility/framework/EditableContentDescViewCheck.java
+++ b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/EditableContentDescViewCheck.java
@@ -37,16 +37,17 @@
TextView textView = (TextView) view;
if ((textView.getEditableText() != null)) {
if (!TextUtils.isEmpty(textView.getContentDescription())) {
- results.add(new AccessibilityViewCheckResult(this, AccessibilityCheckResultType.ERROR,
+ results.add(new AccessibilityViewCheckResult(this.getClass(),
+ AccessibilityCheckResultType.ERROR,
"Editable TextView should not have a contentDescription.", textView));
}
} else {
- results.add(new AccessibilityViewCheckResult(this, AccessibilityCheckResultType.NOT_RUN,
- "TextView must be editable", textView));
+ results.add(new AccessibilityViewCheckResult(this.getClass(),
+ AccessibilityCheckResultType.NOT_RUN, "TextView must be editable", textView));
}
} else {
- results.add(new AccessibilityViewCheckResult(this, AccessibilityCheckResultType.NOT_RUN,
- "View must be a TextView", view));
+ results.add(new AccessibilityViewCheckResult(this.getClass(),
+ AccessibilityCheckResultType.NOT_RUN, "View must be a TextView", view));
}
return results;
diff --git a/src/main/java/com/google/android/apps/common/testing/accessibility/framework/SpeakableTextPresentInfoCheck.java b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/SpeakableTextPresentInfoCheck.java
index 99f985a..f048141 100644
--- a/src/main/java/com/google/android/apps/common/testing/accessibility/framework/SpeakableTextPresentInfoCheck.java
+++ b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/SpeakableTextPresentInfoCheck.java
@@ -1,16 +1,14 @@
/*
* Copyright (C) 2014 Google Inc.
*
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
+ * 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
+ * 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.
*/
@@ -18,23 +16,57 @@
import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType;
+import android.content.Context;
+import android.support.v4.view.ViewPager;
+import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.text.TextUtils;
+import android.view.ViewGroup;
import android.view.accessibility.AccessibilityNodeInfo;
+import android.webkit.WebView;
+import android.widget.ListView;
+import android.widget.ScrollView;
+
+import com.googlecode.eyesfree.utils.AccessibilityNodeInfoUtils;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
/**
* Check to ensure that an info has speakable text for a screen reader
*/
public class SpeakableTextPresentInfoCheck extends AccessibilityInfoCheck {
+ private static List<Class<? extends ViewGroup>> blacklistedViewTypes =
+ /*
+ * TODO(pweaver) Revisit this list once we have robust testing for info checks. It should
+ * only contain classes that can't be handled any other way. ShouldFocusNode likely
+ * handles many of them, for example.
+ */
+ Arrays.asList(ListView.class, ScrollView.class, ViewPager.class, WebView.class);
@Override
- public List<AccessibilityInfoCheckResult> runCheckOnInfo(AccessibilityNodeInfo info) {
+ public List<AccessibilityInfoCheckResult> runCheckOnInfo(AccessibilityNodeInfo info,
+ Context context) {
List<AccessibilityInfoCheckResult> results = new ArrayList<AccessibilityInfoCheckResult>();
- if (TextUtils.isEmpty(AccessibilityCheckUtils.getSpeakableTextForInfo(info))) {
- results.add(new AccessibilityInfoCheckResult(this, AccessibilityCheckResultType.ERROR,
- "View is missing speakable text needed for a screen reader", info));
+ AccessibilityNodeInfoCompat compatInfo = new AccessibilityNodeInfoCompat(info);
+ for (Class<? extends ViewGroup> clazz : blacklistedViewTypes) {
+ if (AccessibilityNodeInfoUtils.nodeMatchesAnyClassByType(null, compatInfo, clazz)) {
+ String msg =
+ String.format("Views of type %s are not checked for speakable text.", clazz.getName());
+ results.add(new AccessibilityInfoCheckResult(this.getClass(),
+ AccessibilityCheckResultType.NOT_RUN, msg, info));
+ return results;
+ }
+ }
+ if (AccessibilityNodeInfoUtils.shouldFocusNode(context, compatInfo)) {
+ if (TextUtils.isEmpty(AccessibilityCheckUtils.getSpeakableTextForInfo(info))) {
+ results.add(new AccessibilityInfoCheckResult(this.getClass(),
+ AccessibilityCheckResultType.ERROR,
+ "View is missing speakable text needed for a screen reader", info));
+ }
+ } else {
+ results.add(new AccessibilityInfoCheckResult(this.getClass(),
+ AccessibilityCheckResultType.NOT_RUN, "View is not focused by screen readers", info));
}
return results;
}
diff --git a/src/main/java/com/google/android/apps/common/testing/accessibility/framework/SpeakableTextPresentViewCheck.java b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/SpeakableTextPresentViewCheck.java
index 7de3708..5c99eee 100644
--- a/src/main/java/com/google/android/apps/common/testing/accessibility/framework/SpeakableTextPresentViewCheck.java
+++ b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/SpeakableTextPresentViewCheck.java
@@ -1,16 +1,14 @@
/*
* Copyright (C) 2014 Google Inc.
*
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
+ * 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
+ * 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.
*/
@@ -18,22 +16,66 @@
import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType;
+import android.os.Build;
+import android.support.v4.view.ViewPager;
import android.text.TextUtils;
import android.view.View;
+import android.view.ViewGroup;
+import android.webkit.WebView;
+import android.widget.ListView;
+import android.widget.ScrollView;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
/**
* Check to ensure that a view has a content description for a screen reader
*/
public class SpeakableTextPresentViewCheck extends AccessibilityViewCheck {
+ /*
+ * TODO(pweaver) Determine if we can reduce this list, and add notes for those that need to
+ * be here.
+ * WebView: This class doesn't behave consistently across platforms, and it can report
+ * that it has no children even when it is displaying content.
+ */
+ private static final List<Class<? extends ViewGroup>> blacklistedViewTypes =
+ Arrays.asList(ListView.class, ScrollView.class, ViewPager.class, WebView.class);
+
@Override
public List<AccessibilityViewCheckResult> runCheckOnView(View view) {
List<AccessibilityViewCheckResult> results = new ArrayList<AccessibilityViewCheckResult>();
- if (TextUtils.isEmpty(AccessibilityCheckUtils.getSpeakableTextForView(view))) {
- results.add(new AccessibilityViewCheckResult(this, AccessibilityCheckResultType.ERROR,
- "View is missing speakable text needed for a screen reader", view));
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
+ results.add(new AccessibilityViewCheckResult(this.getClass(),
+ AccessibilityCheckResultType.NOT_RUN, "This check only runs on Android 4.1 and above.",
+ view));
+ return results;
+ }
+ if (!ViewAccessibilityUtils.isImportantForAccessibility(view)) {
+ results.add(new AccessibilityViewCheckResult(this.getClass(),
+ AccessibilityCheckResultType.NOT_RUN, "View is not important for accessibility.", view));
+ return results;
+ }
+ for (Class<? extends ViewGroup> clazz : blacklistedViewTypes) {
+ if (clazz.isAssignableFrom(view.getClass())) {
+ String msg =
+ String.format("Views of type %s are not checked for speakable text.", clazz.getName());
+ results.add(new AccessibilityViewCheckResult(this.getClass(),
+ AccessibilityCheckResultType.NOT_RUN, msg, view));
+ return results;
+ }
+ }
+ if (ViewAccessibilityUtils.shouldFocusView(view)) {
+ // We must evaluate this view for speakable text
+ if (TextUtils.isEmpty(AccessibilityCheckUtils.getSpeakableTextForView(view))) {
+ results.add(new AccessibilityViewCheckResult(this.getClass(),
+ AccessibilityCheckResultType.ERROR,
+ "View is missing speakable text needed for a screen reader", view));
+ }
+ } else {
+ results.add(new AccessibilityViewCheckResult(this.getClass(),
+ AccessibilityCheckResultType.NOT_RUN, "View is not focused by a screen reader", view));
}
return results;
}
diff --git a/src/main/java/com/google/android/apps/common/testing/accessibility/framework/TextContrastViewCheck.java b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/TextContrastViewCheck.java
index 4122370..3bac93e 100644
--- a/src/main/java/com/google/android/apps/common/testing/accessibility/framework/TextContrastViewCheck.java
+++ b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/TextContrastViewCheck.java
@@ -1,16 +1,14 @@
/*
* Copyright (C) 2014 Google Inc.
*
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
+ * 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
+ * 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.
*/
@@ -22,6 +20,7 @@
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
+import android.text.TextUtils;
import android.view.View;
import android.widget.TextView;
@@ -29,6 +28,7 @@
import java.util.ArrayList;
import java.util.List;
+import java.util.Locale;
/**
* Check to ensure that a TextView has sufficient contrast between text color and background color
@@ -39,7 +39,7 @@
public List<AccessibilityViewCheckResult> runCheckOnView(View view) {
ArrayList<AccessibilityViewCheckResult> results = new ArrayList<AccessibilityViewCheckResult>();
// TODO(caseyburkhardt): Implement this for other types of views
- if (view instanceof TextView) {
+ if ((view instanceof TextView) && (!TextUtils.isEmpty(((TextView) view).getText()))) {
TextView textView = (TextView) view;
int textColor = textView.getCurrentTextColor();
Drawable background = textView.getBackground();
@@ -53,34 +53,36 @@
ContrastUtils.calculateLuminance(textColor),
ContrastUtils.calculateLuminance(backgroundColor));
double backgroundAlpha = Color.alpha(backgroundColor);
- double requiredContrast = isLargeText(textView)
- ? ContrastUtils.CONTRAST_RATIO_WCAG_LARGE_TEXT
- : ContrastUtils.CONTRAST_RATIO_WCAG_NORMAL_TEXT;
+ double requiredContrast =
+ isLargeText(textView) ? ContrastUtils.CONTRAST_RATIO_WCAG_LARGE_TEXT
+ : ContrastUtils.CONTRAST_RATIO_WCAG_NORMAL_TEXT;
if (contrast < requiredContrast) {
if (backgroundAlpha < 255) {
// Cannot guarantee contrast ratio if the background is not opaque
String message = "View's background color must be opaque";
- results.add(new AccessibilityViewCheckResult(
- this, AccessibilityCheckResultType.NOT_RUN, message, view));
+ results.add(new AccessibilityViewCheckResult(this.getClass(),
+ AccessibilityCheckResultType.NOT_RUN, message, view));
} else {
- String message = String.format("TextView does not have required contrast of "
- + "%f. Actual contrast is %f", requiredContrast, contrast);
- results.add(new AccessibilityViewCheckResult(
- this, AccessibilityCheckResultType.ERROR, message, view));
+ String message = String.format(Locale.US,
+ "TextView does not have required contrast of " + "%f. Actual contrast is %f",
+ requiredContrast, contrast);
+ results.add(new AccessibilityViewCheckResult(this.getClass(),
+ AccessibilityCheckResultType.ERROR, message, view));
}
}
} else {
String message = "Cannot be run on API levels lower than 11";
- results.add(new AccessibilityViewCheckResult(
- this, AccessibilityCheckResultType.NOT_RUN, message, view));
+ results.add(new AccessibilityViewCheckResult(this.getClass(),
+ AccessibilityCheckResultType.NOT_RUN, message, view));
}
- } else { // non-solid background color
- results.add(new AccessibilityViewCheckResult(this, AccessibilityCheckResultType.NOT_RUN,
- "TextView does not have a solid background color", view));
+ } else { // non-solid background color
+ results.add(new AccessibilityViewCheckResult(this.getClass(),
+ AccessibilityCheckResultType.NOT_RUN, "TextView does not have a solid background color",
+ view));
}
} else {
- results.add(new AccessibilityViewCheckResult(this, AccessibilityCheckResultType.NOT_RUN,
- "View must be a TextView", view));
+ results.add(new AccessibilityViewCheckResult(this.getClass(),
+ AccessibilityCheckResultType.NOT_RUN, "View must be a non-empty TextView", view));
}
return results;
}
@@ -91,9 +93,9 @@
*/
private static boolean isLargeText(TextView textView) {
float textSize = textView.getTextSize();
- if ((textSize >= ContrastUtils.WCAG_LARGE_TEXT_MIN_SIZE)
- || ((textSize >= ContrastUtils.WCAG_LARGE_BOLD_TEXT_MIN_SIZE)
- && textView.getTypeface().isBold())) {
+ if ((textSize >= ContrastUtils.WCAG_LARGE_TEXT_MIN_SIZE) || (
+ (textSize >= ContrastUtils.WCAG_LARGE_BOLD_TEXT_MIN_SIZE)
+ && textView.getTypeface().isBold())) {
return true;
}
return false;
diff --git a/src/main/java/com/google/android/apps/common/testing/accessibility/framework/TouchTargetSizeViewCheck.java b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/TouchTargetSizeViewCheck.java
index 1cceced..2504d8f 100644
--- a/src/main/java/com/google/android/apps/common/testing/accessibility/framework/TouchTargetSizeViewCheck.java
+++ b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/TouchTargetSizeViewCheck.java
@@ -1,16 +1,14 @@
/*
* Copyright (C) 2014 Google Inc.
*
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
+ * 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
+ * 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.
*/
@@ -19,6 +17,7 @@
import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType;
import android.view.View;
+import android.view.ViewParent;
import java.util.ArrayList;
import java.util.List;
@@ -29,8 +28,8 @@
public class TouchTargetSizeViewCheck extends AccessibilityViewCheck {
/**
- * Minimum height and width are set according to
- * <a href="http://developer.android.com/design/patterns/accessibility.html"></a>
+ * Minimum height and width are set according to <a
+ * href="http://developer.android.com/design/patterns/accessibility.html"></a>
*/
private static final int TOUCH_TARGET_MIN_HEIGHT = 48;
private static final int TOUCH_TARGET_MIN_WIDTH = 48;
@@ -39,23 +38,69 @@
public List<AccessibilityViewCheckResult> runCheckOnView(View view) {
ArrayList<AccessibilityViewCheckResult> results = new ArrayList<AccessibilityViewCheckResult>();
+ if (!(view.isClickable() || view.isLongClickable())) {
+ results.add(new AccessibilityViewCheckResult(this.getClass(),
+ AccessibilityCheckResultType.NOT_RUN, "View is not clickable", view));
+ return results;
+ }
+
+ if (!ViewAccessibilityUtils.isVisibleToUser(view)) {
+ results.add(new AccessibilityViewCheckResult(this.getClass(),
+ AccessibilityCheckResultType.NOT_RUN, "View is not visible", view));
+ return results;
+ }
+
// dp calculation is pixels/density
float density = view.getContext().getResources().getDisplayMetrics().density;
float targetHeight = view.getHeight() / density;
float targetWidth = view.getWidth() / density;
- // If this object passes touches to a different (possibly larger) object, don't check its bounds
- if (view.getTouchDelegate() == null) {
- if (targetHeight < TOUCH_TARGET_MIN_HEIGHT || targetWidth < TOUCH_TARGET_MIN_WIDTH) {
- String message = String.format("View is too small of a touch target. Minimum touch target "
- + "size is %dx%ddp. Actual size is %.1fx%.1fdp (screen density is %.1f).",
- TOUCH_TARGET_MIN_WIDTH, TOUCH_TARGET_MIN_HEIGHT, targetWidth, targetHeight, density);
- results.add(new AccessibilityViewCheckResult(
- this, AccessibilityCheckResultType.ERROR, message, view));
+
+ if (targetHeight < TOUCH_TARGET_MIN_HEIGHT || targetWidth < TOUCH_TARGET_MIN_WIDTH) {
+ // Before we know a view fails this check, we must check if one of the view's ancestors may be
+ // handling touches on its behalf.
+ boolean hasDelegate = hasAncestorWithTouchDelegate(view);
+
+ // We can't get the delegated view from a TouchDelegate, so any TouchDelegate in the view's
+ // lineage will demote ERROR to WARNING.
+ AccessibilityCheckResultType resultType =
+ hasDelegate ? AccessibilityCheckResultType.WARNING : AccessibilityCheckResultType.ERROR;
+
+ StringBuilder messageBuilder = new StringBuilder(String.format(
+ "View falls below the minimum recommended size for touch targets. Minimum touch target "
+ + "size is %dx%ddp. Actual size is %.1fx%.1fdp (screen density is %.1f).",
+ TOUCH_TARGET_MIN_WIDTH,
+ TOUCH_TARGET_MIN_HEIGHT,
+ targetWidth,
+ targetHeight,
+ density));
+ if (hasDelegate) {
+ messageBuilder.append(
+ " A TouchDelegate has been detected on one of this view's ancestors. If the delegate "
+ + "is of sufficient size and handles touches for this view, this warning may be "
+ + "ignored.");
}
- } else {
- results.add(new AccessibilityViewCheckResult(this, AccessibilityCheckResultType.NOT_RUN,
- "View must not have a TouchDelegate", view));
+
+ results.add(
+ new AccessibilityViewCheckResult(this.getClass(), resultType, messageBuilder, view));
}
+
return results;
}
+
+ private static boolean hasAncestorWithTouchDelegate(View view) {
+ if (view == null) {
+ return false;
+ }
+
+ View evalView = null;
+ ViewParent parent = view.getParent();
+ if (parent instanceof View) {
+ evalView = (View) parent;
+ if (evalView.getTouchDelegate() != null) {
+ return true;
+ }
+ }
+
+ return hasAncestorWithTouchDelegate(evalView);
+ }
}