Adding files omitted from previous commit and needed for 2.0.
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 c21ad2d..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
@@ -14,6 +14,8 @@
 
 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
@@ -107,4 +109,22 @@
     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/AccessibilityEventCheck.java b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityEventCheck.java
new file mode 100644
index 0000000..0ca8bbe
--- /dev/null
+++ b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityEventCheck.java
@@ -0,0 +1,119 @@
+/*
+ * 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;
+
+import android.view.accessibility.AccessibilityEvent;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * An {@code AccessibilityEventCheck} is used as a mechanism for detecting accessibility issues
+ * based on the {@link AccessibilityEvent}s dispatched by an application or UI component.
+ * <p>
+ * Extending classes should use {@link #shouldHandleEvent(AccessibilityEvent)} as a convenience
+ * mechanism for filtering events which they are interested in evaluating. If {@code true} is
+ * returned, the event will be passed to {@link #runCheckOnEvent(AccessibilityEvent)} for
+ * evaluation. Much like other {@link AccessibilityCheck}s, results are returned through a single
+ * object, in this case {@link AccessibilityEventCheckResult}, which may include the culprit
+ * {@link AccessibilityEvent}.
+ * <p>
+ * {@code AccessibilityEventCheck}s are different from other {@link AccessibilityCheck}s in that
+ * they may maintain state internal to the class. This is because {@code AccessibilityEventCheck}s
+ * operate on a stream of {@link AccessibilityEvent}s, and ordering, timing, or comparison of
+ * multiple events may be required to properly evaluate an accessibility issue. The class defines
+ * several callback methods to simplify managing this state. The component responsible for executing
+ * this check must invoke {@link #onExecutionStarted()} when a new logical "test run" begins. It
+ * must also invoke {@link #onExecutionEnded()} as it ends. Extending classes may wish to clean up
+ * any state and return any final results from this method.
+ * <p>
+ * NOTE: Although extending classes can access all AccessibilityEvents fired by the system during
+ * the test run interval, no guarantees about stability of an underlying UI are made. Information
+ * about the current view hierarchy state may be accessed via
+ * {@link AccessibilityEvent#getSource()}, but implementations must take care when determining when
+ * and how to maintain state related to this information across invocations of
+ * {@link #runCheckOnEvent(AccessibilityEvent)}. A general recommendation is to only store
+ * information related to the {@link AccessibilityEvent} stream as part of the state maintained by
+ * an extension of this class. To write an {@link AccessibilityCheck} that verifies some properties
+ * of a view hierarchy, use an {@link AccessibilityViewCheck} or {@link AccessibilityInfoCheck}.
+ */
+public abstract class AccessibilityEventCheck extends AccessibilityCheck {
+
+  /**
+   * Convenience method for easily filtering the {@link AccessibilityEvent}s to be dispatched to
+   * {@link #runCheckOnEvent(AccessibilityEvent)} for evaluation.
+   * <p>
+   * NOTE: The default implementation accepts all incoming events.
+   *
+   * @param event The event to filter
+   * @return {@code true} if this {@code AccessibilityEventCheck} should handle this event,
+   *         {@code false} otherwise.
+   */
+  protected boolean shouldHandleEvent(AccessibilityEvent event) {
+    return true;
+  }
+
+  /**
+   * Invoked when a new logical test run is beginning. Implementing checks should use this method to
+   * initialize resources or state needed for evaluation, if needed.
+   * {@link #shouldHandleEvent(AccessibilityEvent)} and {@link #runCheckOnEvent(AccessibilityEvent)}
+   * are guaranteed to not be invoked until execution of this method terminates.
+   */
+  public void onExecutionStarted() {}
+
+  /**
+   * Invoked by the component responsible for executing this {@code AccessibilityCheck} to dispatch
+   * an {@link AccessibilityEvent} to this check's logic.
+   *
+   * @param event The event to dispatch
+   * @return A {@link List} of {@link AccessibilityEventCheckResult}s generated by the check. If no
+   *         such results are generated, {@code null} or an empty collection may be returned.
+   */
+  public final List<AccessibilityEventCheckResult> dispatchEvent(AccessibilityEvent event) {
+    if (shouldHandleEvent(event)) {
+      return runCheckOnEvent(event);
+    }
+    return null;
+  }
+
+  /**
+   * Mechanism by which {@link AccessibilityEvent}s are delivered for evaluation. Extending classes
+   * should override this method and return {@link AccessibilityEventCheckResult}s to indicate
+   * results, if appropriate.
+   *
+   * @param event The event to evaluate.
+   * @return A List of {@link AccessibilityEventCheckResult}s indicating an accessibility issue, if
+   *         any. If no issues are found, or if an issue cannot be identified from the stream of
+   *         {@link AccessibilityEvent}s observed, {@code null} or an empty collection may be
+   *         returned.
+   */
+  protected abstract List<AccessibilityEventCheckResult> runCheckOnEvent(AccessibilityEvent event);
+
+  /**
+   * Invoked when a logical test run has concluded. Implementing checks should use this to clear any
+   * state relevant to the previous evaluation, if needed. It is guaranteed that
+   * {@link #shouldHandleEvent(AccessibilityEvent)} or {@link #runCheckOnEvent(AccessibilityEvent)}
+   * will not be invoked after execution of this method has begun until a new logical test run is
+   * signaled by {@link #onExecutionStarted()};
+   *
+   * @return A List of {@link AccessibilityEventCheckResult}s indicating accessibility issues, if
+   *         any. If no issues are found, or if an issue cannot be identified from the stream of
+   *         {@link AccessibilityEvent}s observed, {@code null} or an empty collection may be
+   *         returned.
+   */
+  public List<AccessibilityEventCheckResult> onExecutionEnded() {
+    return Collections.emptyList();
+  }
+}
diff --git a/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityEventCheckResult.java b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityEventCheckResult.java
new file mode 100644
index 0000000..6ba00bd
--- /dev/null
+++ b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityEventCheckResult.java
@@ -0,0 +1,131 @@
+/*
+ * 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;
+
+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.AccessibilityEvent;
+
+import com.googlecode.eyesfree.utils.LogUtils;
+
+/**
+ * Result generated when an accessibility check runs on a {@link AccessibilityEvent}.
+ */
+@TargetApi(Build.VERSION_CODES.DONUT)
+public final class AccessibilityEventCheckResult extends AccessibilityCheckResult implements
+    Parcelable {
+
+  private AccessibilityEvent event;
+
+  /**
+   * @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 event The {@link AccessibilityEvent} reported as the cause of the result
+   */
+  public AccessibilityEventCheckResult(Class<? extends AccessibilityCheck> checkClass,
+      AccessibilityCheckResultType type, CharSequence message, AccessibilityEvent event) {
+    super(checkClass, type, message);
+    if (event != null) {
+      this.event = AccessibilityEvent.obtain(event);
+    }
+  }
+
+  private AccessibilityEventCheckResult(Parcel in) {
+    super(null, null, null);
+    readFromParcel(in);
+  }
+
+  /**
+   * @return The {@link AccessibilityEvent} to which the result applies
+   */
+  public AccessibilityEvent getEvent() {
+    return event;
+  }
+
+  @Override
+  public void recycle() {
+    super.recycle();
+    if (event != null) {
+      event.recycle();
+      event = 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);
+
+    // Event requires a presence flag
+    if (event != null) {
+      dest.writeInt(1);
+      event.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);
+
+    // Event
+    this.event = (in.readInt() == 1) ? AccessibilityEvent.CREATOR.createFromParcel(in) : null;
+  }
+
+  public static final Parcelable.Creator<AccessibilityEventCheckResult> CREATOR =
+      new Parcelable.Creator<AccessibilityEventCheckResult>() {
+        @Override
+        public AccessibilityEventCheckResult createFromParcel(Parcel in) {
+          return new AccessibilityEventCheckResult(in);
+        }
+
+        @Override
+        public AccessibilityEventCheckResult[] newArray(int size) {
+          return new AccessibilityEventCheckResult[size];
+        }
+      };
+}
diff --git a/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityViewCheckException.java b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityViewCheckException.java
new file mode 100644
index 0000000..86b6be8
--- /dev/null
+++ b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AccessibilityViewCheckException.java
@@ -0,0 +1,91 @@
+/*
+ * 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;
+
+import android.view.View;
+
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * An exception class to be used for throwing exceptions with accessibility results. This class can
+ * be extended to provide descriptions of {@link AccessibilityViewCheckResult}s that the developer
+ * considers readable. To extend this class, override the constructor and call super, and override
+ * {@link #getResultMessage} to provide a readable String description of a result.
+ */
+public class AccessibilityViewCheckException extends RuntimeException {
+  private List<AccessibilityViewCheckResult> results;
+
+  /**
+   * Any extension of this class must call this constructor.
+   *
+   * @param results a list of {@link AccessibilityViewCheckResult}s that are associated with the
+   *        failure(s) that cause this to be thrown.
+   */
+  protected AccessibilityViewCheckException(List<AccessibilityViewCheckResult> results) {
+    super();
+    if ((results == null) || (results.size() == 0)) {
+      throw new RuntimeException(
+          "AccessibilityViewCheckException requires at least 1 AccessibilityViewCheckResult");
+    }
+    this.results = results;
+  }
+
+  @Override
+  public String getMessage() {
+    // Lump all error result messages into one string to be the exception message
+    StringBuilder exceptionMessage = new StringBuilder();
+    String errorCountMessage = (results.size() == 1)
+        ? "There was 1 accessibility error:\n"
+        : String.format(Locale.US, "There were %d accessibility errors:\n", results.size());
+    exceptionMessage.append(errorCountMessage);
+    for (int i = 0; i < results.size(); i++) {
+      if (i > 0) {
+        exceptionMessage.append(",\n");
+      }
+      AccessibilityViewCheckResult result = results.get(i);
+      exceptionMessage.append(getResultMessage(result));
+    }
+    return exceptionMessage.toString();
+  }
+
+  /**
+   * @return the list of results associated with this instance
+   */
+  public List<AccessibilityViewCheckResult> getResults() {
+    return results;
+  }
+
+  /**
+   * Returns a String description of the given {@link AccessibilityViewCheckResult}. The default
+   * is to return the view's resource entry name followed by the result's message.
+   *
+   * @param result the {@link AccessibilityViewCheckResult} to describe
+   * @return a String description of the result
+   */
+  protected String getResultMessage(AccessibilityViewCheckResult result) {
+    StringBuilder msg = new StringBuilder();
+    View view = result.getView();
+    if ((view != null) && (view.getId() != View.NO_ID) && (view.getResources() != null)) {
+      msg.append("View ");
+      msg.append(view.getResources().getResourceEntryName(view.getId()));
+      msg.append(": ");
+    } else {
+      msg.append("View with no valid resource name: ");
+    }
+    msg.append(result.getMessage());
+    return msg.toString();
+  }
+}
diff --git a/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AnnouncementEventCheck.java b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AnnouncementEventCheck.java
new file mode 100644
index 0000000..102c892
--- /dev/null
+++ b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/AnnouncementEventCheck.java
@@ -0,0 +1,51 @@
+/*
+ * 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;
+
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Check which may be used to flag use of {@link View#announceForAccessibility(CharSequence)} or
+ * dispatch of {@link AccessibilityEvent}s of type {@link AccessibilityEvent#TYPE_ANNOUNCEMENT}. The
+ * use of these events, expect in specific situations, can be disruptive to the user.
+ */
+@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+public class AnnouncementEventCheck extends AccessibilityEventCheck {
+
+  @Override
+  public boolean shouldHandleEvent(AccessibilityEvent event) {
+    return (event != null) && (event.getEventType() == AccessibilityEvent.TYPE_ANNOUNCEMENT);
+  }
+
+  @Override
+  public List<AccessibilityEventCheckResult> runCheckOnEvent(AccessibilityEvent event) {
+    // TODO(caseyburkhardt): Promote multiple qualifying events within a time threshold to
+    // AccessibilityCheckResultType.ERROR
+    // TODO(caseyburkhardt): Develop some heuristic for identifying approved use cases
+    List<AccessibilityEventCheckResult> results = new ArrayList<AccessibilityEventCheckResult>(1);
+    results.add(new AccessibilityEventCheckResult(this.getClass(),
+        AccessibilityCheckResultType.WARNING,
+        "A disruptive accessibility announcement has been used,", event));
+    return results;
+  }
+}
diff --git a/src/main/java/com/google/android/apps/common/testing/accessibility/framework/ClickableSpanInfoCheck.java b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/ClickableSpanInfoCheck.java
new file mode 100644
index 0000000..cc488b3
--- /dev/null
+++ b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/ClickableSpanInfoCheck.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2014 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.apps.common.testing.accessibility.framework;
+
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType;
+
+import android.content.Context;
+import android.net.Uri;
+import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
+import android.text.Spanned;
+import android.text.style.ClickableSpan;
+import android.text.style.URLSpan;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.TextView;
+
+import com.googlecode.eyesfree.utils.AccessibilityNodeInfoUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Check to ensure that {@code ClickableSpan} is not being used in a TextView.
+ *
+ * <p>{@code ClickableSpan} is inaccessible because individual spans cannot be selected
+ * independently in a single TextView and because accessibility services are unable to call
+ * {@link ClickableSpan#onClick}.
+ *
+ * <p>The exception to this rule is that {@code URLSpan}s are accessible if they do not contain a
+ * relative URI.
+ */
+public class ClickableSpanInfoCheck extends AccessibilityInfoCheck {
+
+  @Override
+  public List<AccessibilityInfoCheckResult> runCheckOnInfo(AccessibilityNodeInfo info,
+      Context context) {
+    List<AccessibilityInfoCheckResult> results = new ArrayList<AccessibilityInfoCheckResult>(1);
+    AccessibilityNodeInfoCompat compatInfo = new AccessibilityNodeInfoCompat(info);
+    if (AccessibilityNodeInfoUtils.nodeMatchesAnyClassByType(context, compatInfo, TextView.class)) {
+      if (info.getText() instanceof Spanned) {
+        Spanned text = (Spanned) info.getText();
+        ClickableSpan[] clickableSpans = text.getSpans(0, text.length(), ClickableSpan.class);
+        for (ClickableSpan clickableSpan : clickableSpans) {
+          if (clickableSpan instanceof URLSpan) {
+            String url = ((URLSpan) clickableSpan).getURL();
+            if (url == null) {
+              results.add(new AccessibilityInfoCheckResult(this.getClass(),
+                  AccessibilityCheckResultType.ERROR, "URLSpan has null URL", info));
+            } else {
+              Uri uri = Uri.parse(url);
+              if (uri.isRelative()) {
+                // Relative URIs cannot be resolved.
+                results.add(new AccessibilityInfoCheckResult(this.getClass(),
+                    AccessibilityCheckResultType.ERROR, "URLSpan should not contain relative links",
+                    info));
+              }
+            }
+          } else { // Non-URLSpan ClickableSpan
+            results.add(new AccessibilityInfoCheckResult(this.getClass(),
+                AccessibilityCheckResultType.ERROR,
+                "URLSpan should be used in place of ClickableSpan for improved accessibility",
+                info));
+          }
+        }
+      }
+    } else {
+      results.add(new AccessibilityInfoCheckResult(this.getClass(),
+          AccessibilityCheckResultType.NOT_RUN, "View must be a TextView", info));
+    }
+    return results;
+  }
+}
diff --git a/src/main/java/com/google/android/apps/common/testing/accessibility/framework/ClickableSpanViewCheck.java b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/ClickableSpanViewCheck.java
new file mode 100644
index 0000000..047c8c5
--- /dev/null
+++ b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/ClickableSpanViewCheck.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2014 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.apps.common.testing.accessibility.framework;
+
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType;
+
+import android.net.Uri;
+import android.text.Spanned;
+import android.text.style.ClickableSpan;
+import android.text.style.URLSpan;
+import android.view.View;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Check to ensure that {@code ClickableSpan} is not being used in a TextView.
+ *
+ * <p>{@code ClickableSpan} is inaccessible because individual spans cannot be selected
+ * independently in a single TextView and because accessibility services are unable to call
+ * the OnClick method of a {@code ClickableSpan}.
+ *
+ * <p>The exception to this rule is that {@code URLSpan}s are accessible if they do not contain a
+ * relative URI.
+ */
+public class ClickableSpanViewCheck extends AccessibilityViewCheck {
+
+  @Override
+  public List<AccessibilityViewCheckResult> runCheckOnView(View view) {
+    List<AccessibilityViewCheckResult> results = new ArrayList<AccessibilityViewCheckResult>(1);
+    if (view instanceof TextView) {
+      TextView textView = (TextView) view;
+      if (textView.getText() instanceof Spanned) {
+        Spanned text = (Spanned) textView.getText();
+        ClickableSpan[] clickableSpans = text.getSpans(0, text.length(), ClickableSpan.class);
+        for (ClickableSpan clickableSpan : clickableSpans) {
+          if (clickableSpan instanceof URLSpan) {
+            String url = ((URLSpan) clickableSpan).getURL();
+            if (url == null) {
+              results.add(new AccessibilityViewCheckResult(this.getClass(),
+                  AccessibilityCheckResultType.ERROR, "URLSpan has null URL", view));
+            } else {
+              Uri uri = Uri.parse(url);
+              if (uri.isRelative()) {
+                // Relative URIs cannot be resolved.
+                results.add(new AccessibilityViewCheckResult(this.getClass(),
+                    AccessibilityCheckResultType.ERROR, "URLSpan should not contain relative links",
+                    view));
+              }
+            }
+          } else { // Non-URLSpan ClickableSpan
+            results.add(new AccessibilityViewCheckResult(this.getClass(),
+                AccessibilityCheckResultType.ERROR,
+                "URLSpan should be used in place of ClickableSpan for improved accessibility",
+                view));
+          }
+        }
+      }
+    } else {
+      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/DuplicateClickableBoundsInfoCheck.java b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/DuplicateClickableBoundsInfoCheck.java
new file mode 100644
index 0000000..e83f06d
--- /dev/null
+++ b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/DuplicateClickableBoundsInfoCheck.java
@@ -0,0 +1,75 @@
+/*
+ * 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;
+
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Developers sometimes have containers marked clickable when they don't process click events.
+ * This error is difficult to detect, but when a container shares its bounds with a child view,
+ * that is a clear error. This class catches that case.
+ */
+public class DuplicateClickableBoundsInfoCheck extends AccessibilityInfoHierarchyCheck {
+
+  @Override
+  public List<AccessibilityInfoCheckResult> runCheckOnInfoHierarchy(AccessibilityNodeInfo root,
+      Context context) {
+    List<AccessibilityInfoCheckResult> results = new ArrayList<>(1);
+    Map<Rect, AccessibilityNodeInfo> clickableRectToInfoMap = new HashMap<>();
+
+    checkForDuplicateClickableViews(root, clickableRectToInfoMap, results);
+    for (AccessibilityNodeInfo info : clickableRectToInfoMap.values()) {
+      info.recycle();
+    }
+    return results;
+  }
+
+  private void checkForDuplicateClickableViews(AccessibilityNodeInfo root,
+      Map<Rect, AccessibilityNodeInfo> clickableRectToInfoMap,
+      List<AccessibilityInfoCheckResult> results) {
+    /*
+     * TODO(pweaver) It may be possible for this check to false-negative if one view is marked
+     * clickable and the other is only long clickable and/or has custom actions. Determine if this
+     * limitation applies to real UIs.
+     */
+    if (root.isClickable() && root.isVisibleToUser()) {
+      Rect bounds = new Rect();
+      root.getBoundsInScreen(bounds);
+      if (clickableRectToInfoMap.containsKey(bounds)) {
+        results.add(new AccessibilityInfoCheckResult(this.getClass(),
+            AccessibilityCheckResultType.ERROR,
+            "Clickable view has same bounds as another clickable view (likely a descendent)",
+            clickableRectToInfoMap.get(bounds)));
+      } else {
+        clickableRectToInfoMap.put(bounds, AccessibilityNodeInfo.obtain(root));
+      }
+    }
+
+    for (int i = 0; i < root.getChildCount(); ++i) {
+      AccessibilityNodeInfo child = root.getChild(i);
+      checkForDuplicateClickableViews(child, clickableRectToInfoMap, results);
+      child.recycle();
+    }
+  }
+}
diff --git a/src/main/java/com/google/android/apps/common/testing/accessibility/framework/DuplicateClickableBoundsViewCheck.java b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/DuplicateClickableBoundsViewCheck.java
new file mode 100644
index 0000000..97bcd17
--- /dev/null
+++ b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/DuplicateClickableBoundsViewCheck.java
@@ -0,0 +1,79 @@
+/*
+ * 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;
+
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType;
+
+import android.graphics.Rect;
+import android.os.Build;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Developers sometimes have containers marked clickable when they don't process click events.
+ * This error is difficult to detect, but when a container shares its bounds with a child view,
+ * that is a clear error. This class catches that case.
+ */
+public class DuplicateClickableBoundsViewCheck extends AccessibilityViewHierarchyCheck {
+
+  @Override
+  public List<AccessibilityViewCheckResult> runCheckOnViewHierarchy(View root) {
+    List<AccessibilityViewCheckResult> results = new ArrayList<>(1);
+    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 2.3.3 and above.",
+          root));
+      return results;
+    }
+    Map<Rect, View> clickableRectToViewMap = new HashMap<>();
+
+    checkForDuplicateClickableViews(root, clickableRectToViewMap, results);
+    return results;
+  }
+
+  private void checkForDuplicateClickableViews(View root, Map<Rect, View> clickableRectToViewMap,
+      List<AccessibilityViewCheckResult> results) {
+    if (!ViewAccessibilityUtils.isVisibleToUser(root)) {
+      return;
+    }
+    if (root.isClickable() && ViewAccessibilityUtils.isImportantForAccessibility(root)) {
+      Rect bounds = new Rect();
+      if (root.getGlobalVisibleRect(bounds)) {
+        if (clickableRectToViewMap.containsKey(bounds)) {
+          results.add(new AccessibilityViewCheckResult(this.getClass(),
+              AccessibilityCheckResultType.ERROR,
+              "Clickable view has same bounds as another clickable view (likely a descendent)",
+              clickableRectToViewMap.get(bounds)));
+        } else {
+          clickableRectToViewMap.put(bounds, root);
+        }
+      }
+    }
+    if (!(root instanceof ViewGroup)) {
+      return;
+    }
+    ViewGroup viewGroup = (ViewGroup) root;
+    for (int i = 0; i < viewGroup.getChildCount(); ++i) {
+      View child = viewGroup.getChildAt(i);
+      checkForDuplicateClickableViews(child, clickableRectToViewMap, results);
+    }
+  }
+}
diff --git a/src/main/java/com/google/android/apps/common/testing/accessibility/framework/RedundantContentDescInfoCheck.java b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/RedundantContentDescInfoCheck.java
new file mode 100644
index 0000000..eaec370
--- /dev/null
+++ b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/RedundantContentDescInfoCheck.java
@@ -0,0 +1,72 @@
+/*
+ * 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;
+
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType;
+
+import android.content.Context;
+import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
+import android.text.TextUtils;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.Button;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Checks to ensure that speakable text does not contain redundant information about the view's
+ * type. Accessibility services are aware of the view's type and can use that information as needed
+ * (ex: Screen readers may append "button" to the speakable text of a {@link Button}).
+ */
+public class RedundantContentDescInfoCheck extends AccessibilityInfoHierarchyCheck {
+  private static List<CharSequence> redundantWords = new ArrayList<>();
+  static {
+    redundantWords.add("button");
+  }
+
+  @Override
+  public List<AccessibilityInfoCheckResult> runCheckOnInfoHierarchy(AccessibilityNodeInfo root,
+      Context context) {
+    List<AccessibilityInfoCheckResult> results = new ArrayList<AccessibilityInfoCheckResult>();
+    // TODO(sjrush): This check needs internationalization support
+    if (!Locale.getDefault().getLanguage().equals(Locale.ENGLISH.getLanguage())) {
+      results.add(new AccessibilityInfoCheckResult(this.getClass(),
+          AccessibilityCheckResultType.NOT_RUN, "This check only runs in English locales", root));
+      return results;
+    }
+
+    List<AccessibilityNodeInfoCompat> compatInfos = getAllInfoCompatsInHierarchy(context, root);
+    for (AccessibilityNodeInfoCompat compatInfo : compatInfos) {
+      AccessibilityNodeInfo info = (AccessibilityNodeInfo) compatInfo.getInfo();
+      CharSequence contentDescription = info.getContentDescription();
+      if (TextUtils.isEmpty(contentDescription)) {
+        results.add(new AccessibilityInfoCheckResult(this.getClass(),
+            AccessibilityCheckResultType.NOT_RUN, "View has no content description", info));
+        continue;
+      }
+      for (CharSequence redundantWord : redundantWords) {
+        if (contentDescription.toString().toLowerCase().contains(redundantWord)) {
+          results.add(new AccessibilityInfoCheckResult(this.getClass(),
+              AccessibilityCheckResultType.WARNING,
+              "View's speakable text ends with view type",
+              info));
+          break;
+        }
+      }
+    }
+    return results;
+  }
+}
diff --git a/src/main/java/com/google/android/apps/common/testing/accessibility/framework/RedundantContentDescViewCheck.java b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/RedundantContentDescViewCheck.java
new file mode 100644
index 0000000..2852457
--- /dev/null
+++ b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/RedundantContentDescViewCheck.java
@@ -0,0 +1,78 @@
+/*
+ * 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;
+
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType;
+
+import android.os.Build;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.Button;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Checks to ensure that speakable text does not contain redundant information about the view's
+ * type. Accessibility services are aware of the view's type and can use that information as needed
+ * (ex: Screen readers may append "button" to the speakable text of a {@link Button}).
+ */
+public class RedundantContentDescViewCheck extends AccessibilityViewHierarchyCheck {
+  private static List<CharSequence> redundantWords = new ArrayList<>();
+  static {
+    redundantWords.add("button");
+  }
+
+  @Override
+  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 Android 4.1 and above.",
+          root));
+      return results;
+    }
+    // TODO(sjrush): This check needs internationalization support
+    if (!Locale.getDefault().getLanguage().equals(Locale.ENGLISH.getLanguage())) {
+      results.add(new AccessibilityViewCheckResult(this.getClass(),
+          AccessibilityCheckResultType.NOT_RUN, "This check only runs in English locales", root));
+      return results;
+    }
+
+    for (View view : ViewAccessibilityUtils.getAllViewsInHierarchy(root)) {
+      if (!ViewAccessibilityUtils.isImportantForAccessibility(view)) {
+        results.add(new AccessibilityViewCheckResult(this.getClass(),
+            AccessibilityCheckResultType.NOT_RUN, "View is not important for accessibility", view));
+        continue;
+      }
+      CharSequence contentDescription = view.getContentDescription();
+      if (TextUtils.isEmpty(contentDescription)) {
+        results.add(new AccessibilityViewCheckResult(this.getClass(),
+            AccessibilityCheckResultType.NOT_RUN, "View has no content description", view));
+        continue;
+      }
+      for (CharSequence redundantWord : redundantWords) {
+        if (contentDescription.toString().toLowerCase().contains(redundantWord)) {
+          results.add(new AccessibilityViewCheckResult(this.getClass(),
+              AccessibilityCheckResultType.WARNING,
+              "View's speakable text ends with view type",
+              view));
+        }
+      }
+    }
+    return results;
+  }
+}
diff --git a/src/main/java/com/google/android/apps/common/testing/accessibility/framework/TouchTargetSizeInfoCheck.java b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/TouchTargetSizeInfoCheck.java
new file mode 100644
index 0000000..32a7a5d
--- /dev/null
+++ b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/TouchTargetSizeInfoCheck.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2014 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.android.apps.common.testing.accessibility.framework;
+
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import com.googlecode.eyesfree.utils.AccessibilityNodeInfoUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Check to ensure that a view has a touch target that is at least 48x48dp.
+ */
+public class TouchTargetSizeInfoCheck extends AccessibilityInfoCheck {
+
+  /**
+   * 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;
+
+  @Override
+  public List<AccessibilityInfoCheckResult> runCheckOnInfo(AccessibilityNodeInfo info,
+      Context context) {
+    ArrayList<AccessibilityInfoCheckResult> results = new ArrayList<AccessibilityInfoCheckResult>();
+
+    // TODO(sjrush): Have all info checks use AccessibilityNodeInfoCompat
+    AccessibilityNodeInfoCompat infoCompat = new AccessibilityNodeInfoCompat(info);
+    if (!(AccessibilityNodeInfoUtils.isClickable(infoCompat)
+          || AccessibilityNodeInfoUtils.isLongClickable(infoCompat))) {
+      results.add(new AccessibilityInfoCheckResult(this.getClass(),
+          AccessibilityCheckResultType.NOT_RUN, "View is not clickable", info));
+      return results;
+    }
+    if (context == null) {
+      results.add(new AccessibilityInfoCheckResult(this.getClass(),
+          AccessibilityCheckResultType.NOT_RUN, "This check needs a context", info));
+      return results;
+    }
+
+    // TODO(sjrush): Find a way to make this check work without a context
+    // dp calculation is pixels/density
+    float density = context.getResources().getDisplayMetrics().density;
+    Rect bounds = new Rect();
+    info.getBoundsInScreen(bounds);
+    float targetHeight = bounds.height() / density;
+    float targetWidth = bounds.width() / density;
+    if (targetHeight < TOUCH_TARGET_MIN_HEIGHT || targetWidth < TOUCH_TARGET_MIN_WIDTH) {
+      String message = String.format(Locale.US,
+          "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 AccessibilityInfoCheckResult(this.getClass(),
+          AccessibilityCheckResultType.ERROR, message, info));
+    }
+    return results;
+  }
+}
diff --git a/src/main/java/com/google/android/apps/common/testing/accessibility/framework/ViewAccessibilityUtils.java b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/ViewAccessibilityUtils.java
new file mode 100644
index 0000000..d5b7069
--- /dev/null
+++ b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/ViewAccessibilityUtils.java
@@ -0,0 +1,433 @@
+/*
+ * 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;
+
+import android.annotation.TargetApi;
+import android.graphics.Rect;
+import android.os.Build;
+import android.support.v4.view.ViewCompat;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.widget.AdapterView;
+import android.widget.CompoundButton;
+import android.widget.HorizontalScrollView;
+import android.widget.ScrollView;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * This class provides a set of utilities used to evaluate accessibility properties and behaviors of
+ * hierarchies of {@link View}s.
+ */
+@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+final class ViewAccessibilityUtils {
+
+  private ViewAccessibilityUtils() {}
+
+  /**
+   * @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;
+  }
+
+  /**
+   * @see View#isImportantForAccessibility()
+   */
+  public static boolean isImportantForAccessibility(View view) {
+    if (view == null) {
+      return false;
+    }
+
+    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+      return view.isImportantForAccessibility();
+    } else {
+      // On earlier APIs, we must piece together accessibility importance from the available
+      // properties. We return false incorrectly for some cases where unretrievable listeners
+      // prevent us from determining importance.
+
+      // If the developer marked the view as explicitly not important, it isn't.
+      int mode = view.getImportantForAccessibility();
+      if ((mode == View.IMPORTANT_FOR_ACCESSIBILITY_NO)
+          || (mode == View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS)) {
+        return false;
+      }
+
+      // No parent view can be hiding us. (APIs 19 to 21)
+      ViewParent parent = view.getParent();
+      while (parent instanceof View) {
+        if (((View) parent).getImportantForAccessibility()
+            == View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) {
+          return false;
+        }
+        parent = parent.getParent();
+      }
+
+      // Interrogate the view's other properties to determine importance.
+      return (mode == View.IMPORTANT_FOR_ACCESSIBILITY_YES)
+          || isActionableForAccessibility(view)
+          || hasListenersForAccessibility(view)
+          || (view.getAccessibilityNodeProvider() != null)
+          || (ViewCompat.getAccessibilityLiveRegion(view)
+              != ViewCompat.ACCESSIBILITY_LIVE_REGION_NONE);
+    }
+  }
+
+  /**
+   * Determines if the supplied {@link View} is actionable for accessibility purposes.
+   *
+   * @param view The {@link View} to evaluate
+   * @return {@code true} if {@code view} is considered actionable for accessibility
+   */
+  public static boolean isActionableForAccessibility(View view) {
+    if (view == null) {
+      return false;
+    }
+
+    return (view.isClickable() || view.isLongClickable() || view.isFocusable());
+  }
+
+  /**
+   * Determines if the supplied {@link View} is visible to the user, which requires that it be
+   * marked visible, that all its parents are visible, that it and all parents have alpha
+   * greater than 0, and that it has non-zero size. This code attempts to replicate the protected
+   * method {@code View.isVisibleToUser}.
+   *
+   * @param view The {@link View} to evaluate
+   * @return {@code true} if {@code view} is visible to the user
+   */
+  public static boolean isVisibleToUser(View view) {
+    if (view == null) {
+      return false;
+    }
+
+    Object current = view;
+    while (current instanceof View) {
+      View currentView = (View) current;
+      if ((ViewCompat.getAlpha(currentView) <= 0)
+          || (currentView.getVisibility() != View.VISIBLE)) {
+        return false;
+      }
+      current = currentView.getParent();
+    }
+    return view.getGlobalVisibleRect(new Rect());
+  }
+
+  /**
+   * Determines if the supplied {@link View} would be focused during navigation operations with a
+   * screen reader.
+   *
+   * @param view The {@link View} 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(View view) {
+    if (view == null) {
+      return false;
+    }
+
+    if (!isVisibleToUser(view)) {
+      // We don't focus views that are not visible
+      return false;
+    }
+
+    if (isAccessibilityFocusable(view)) {
+      if ((!(view instanceof ViewGroup))
+          || ((view instanceof ViewGroup) && (((ViewGroup) view).getChildCount() == 0))) {
+        // TODO(caseyburkhardt): Do we need to evaluate all ViewGroups and filter non-important
+        // Views to determine leaves? If so, this seems like a rare corner case.
+
+        // 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 non-actionable children)
+        return true;
+      }
+
+      return false;
+    }
+
+    if (hasText(view) && !hasFocusableAncestor(view)) {
+      return true;
+    }
+
+    return false;
+  }
+
+  /**
+   * Find a {@code View}, if one exists, that labels a given {@code View}.
+   *
+   * @param view The target of the labelFor.
+   * @return The {@code View} that is the labelFor the specified view. {@code null}
+   * if nothing labels it.
+   */
+  public static View getLabelForView(View view) {
+    if ((Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1)) {
+      /* Earlier versions don't support labelFor */
+      return null;
+    }
+    int idToFind = view.getId();
+    if (idToFind == View.NO_ID) {
+      /* Views lacking IDs can't be labeled by others */
+      return null;
+    }
+
+    /*
+     * Search for the "nearest" View that labels this one, since IDs aren't unique. This code
+     * follows the framework code by DFSing first children, then siblings, then parent and its
+     * siblings, etc. childToSkip is passed in to the helper method to avoid repeating consideration
+     * of a View when examining its parent.
+     */
+    View childToSkip = null;
+    while (true) {
+      View labelingView = lookForLabelForViewInViewAndChildren(view, childToSkip, idToFind);
+      if (labelingView != null) {
+        return labelingView;
+      }
+      ViewParent parent = view.getParent();
+      childToSkip = view;
+      if (!(parent instanceof View)) {
+        return null;
+      }
+      view = (View) parent;
+    }
+  }
+
+  private static View lookForLabelForViewInViewAndChildren(View view, View childToSkip,
+      int idToFind) {
+    if (view.getLabelFor() == idToFind) {
+      return view;
+    }
+    if (!(view instanceof ViewGroup)) {
+      return null;
+    }
+    ViewGroup viewGroup = (ViewGroup) view;
+    for (int i = 0; i < viewGroup.getChildCount(); ++i) {
+      View child = viewGroup.getChildAt(i);
+      if (!child.equals(childToSkip)) {
+        View labelingView = lookForLabelForViewInViewAndChildren(child, null, idToFind);
+        if (labelingView != null) {
+          return labelingView;
+        }
+      }
+    }
+    return null;
+  }
+
+  /**
+   * 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);
+    }
+  }
+
+  /**
+   * Determines if the supplied {@link View} has any retrievable listeners that might qualify the
+   * view to be important for accessibility purposes.
+   * <p>
+   * NOTE: This method tries to behave like the hidden {@code View#hasListenersForAccessibility()}
+   * method, but cannot retrieve several of the listeners.
+   *
+   * @param view The {@link View} to evaluate
+   * @return {@code true} if any of the retrievable listeners on {@code view} might qualify it to be
+   *         important for accessibility purposes.
+   */
+  private static boolean hasListenersForAccessibility(View view) {
+    if (view == null) {
+      return false;
+    }
+
+    boolean result = false;
+    // Ideally, here we check for...
+    // TouchDelegate
+    result |= view.getTouchDelegate() != null;
+
+    // OnKeyListener, OnTouchListener, OnGenericMotionListener, OnHoverListener, OnDragListener
+    // aren't accessible to us.
+    return result;
+  }
+
+  /**
+   * Determines if the supplied {@link View} 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 View} to evaluate
+   * @return {@code true} if an ancestor of {@code view} may gain accessibility focus, {@code false}
+   *         otherwise
+   */
+  private static boolean hasFocusableAncestor(View view) {
+    if (view == null) {
+      return false;
+    }
+
+    ViewParent parent = view.getParentForAccessibility();
+    if (!(parent instanceof View)) {
+      return false;
+    }
+
+    if (isAccessibilityFocusable((View) parent)) {
+      return true;
+    }
+
+    return hasFocusableAncestor((View) parent);
+  }
+
+  /**
+   * Determines if the supplied {@link View} meets the criteria for gaining accessibility focus.
+   *
+   * @param view The {@link View} to evaluate
+   * @return {@code true} if it is possible for {@code view} to gain accessibility focus,
+   *         {@code false} otherwise.
+   */
+  private static boolean isAccessibilityFocusable(View view) {
+    if (view == null) {
+      return false;
+    }
+
+    if (view.getVisibility() != View.VISIBLE) {
+      return false;
+    }
+
+    if (isActionableForAccessibility(view)) {
+      return true;
+    }
+
+    return isChildOfScrollableContainer(view) && isSpeakingView(view);
+  }
+
+  /**
+   * Determines if the supplied {@link View} is a top-level item within a scrollable container.
+   *
+   * @param view The {@link View} to evaluate
+   * @return {@code true} if {@code view} is a top-level view within a scrollable container,
+   *         {@code false} otherwise
+   */
+  private static boolean isChildOfScrollableContainer(View view) {
+    if (view == null) {
+      return false;
+    }
+
+    ViewParent viewParent = view.getParentForAccessibility();
+    if ((viewParent == null) || !(viewParent instanceof View)) {
+      return false;
+    }
+
+    View parent = (View) viewParent;
+    if (parent.isScrollContainer()) {
+      return true;
+    }
+
+    // Specifically check for parents that are AdapterView, ScrollView, or HorizontalScrollView, but
+    // exclude Spinners, which are a special case of AdapterView.
+    return (((parent instanceof AdapterView) || (parent instanceof ScrollView)
+        || (parent instanceof HorizontalScrollView)) && !(parent instanceof Spinner));
+  }
+
+  /**
+   * Determines if the supplied {@link View} 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 View} to evaluate
+   * @return {@code true} if a spoken description for {@code view} was determined, {@code false}
+   *         otherwise.
+   */
+  private static boolean isSpeakingView(View view) {
+    if (hasText(view)) {
+      return true;
+    } else if (view instanceof CompoundButton) {
+      // Special case for CompoundButton / CheckBox / Switch.
+      return true;
+    } else if (hasNonActionableSpeakingChildren(view)) {
+      return true;
+    }
+
+    return false;
+  }
+
+  /**
+   * Determines if the supplied {@link View} 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 View} to evaluate
+   * @return {@code true} if {@code view} has non-actionable speaking children within its subtree
+   */
+  private static boolean hasNonActionableSpeakingChildren(View view) {
+    if ((view == null) || !(view instanceof ViewGroup)) {
+      return false;
+    }
+
+    ViewGroup group = (ViewGroup) view;
+    for (int i = 0; i < group.getChildCount(); ++i) {
+      View child = group.getChildAt(i);
+      if ((child == null) || (child.getVisibility() != View.VISIBLE)
+          || isAccessibilityFocusable(child)) {
+        continue;
+      }
+
+      if (isImportantForAccessibility(child) && isSpeakingView(child)) {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  /**
+   * Determines if the supplied {@link View} has a contentDescription or text.
+   *
+   * @param view The {@link View} to evaluate
+   * @return {@code true} if {@code view} has a contentDescription or text.
+   */
+  private static boolean hasText(View view) {
+    if (!TextUtils.isEmpty(view.getContentDescription())) {
+      return true;
+    } else if (view instanceof TextView) {
+      return !TextUtils.isEmpty(((TextView) view).getText());
+    }
+
+    return false;
+  }
+}
diff --git a/src/main/java/com/google/android/apps/common/testing/accessibility/framework/integrations/AccessibilityCheckAssertion.java b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/integrations/AccessibilityCheckAssertion.java
new file mode 100644
index 0000000..82444fb
--- /dev/null
+++ b/src/main/java/com/google/android/apps/common/testing/accessibility/framework/integrations/AccessibilityCheckAssertion.java
@@ -0,0 +1,238 @@
+package com.google.android.apps.common.testing.accessibility.framework.integrations;
+
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckPreset;
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResult.AccessibilityCheckResultType;
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils;
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityViewCheckException;
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityViewCheckResult;
+import com.google.android.apps.common.testing.accessibility.framework.AccessibilityViewHierarchyCheck;
+import com.google.android.apps.common.testing.testrunner.InstrumentationArgumentsRegistry;
+import com.google.android.apps.common.testing.ui.espresso.EspressoException;
+import com.google.android.apps.common.testing.ui.espresso.NoMatchingViewException;
+import com.google.android.apps.common.testing.ui.espresso.ViewAssertion;
+import com.google.android.apps.common.testing.ui.espresso.action.ViewActions;
+import com.google.android.apps.common.testing.ui.espresso.util.HumanReadables;
+
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+
+import org.hamcrest.Matcher;
+
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * A class to enable automated accessibility checks in Espresso tests. These checks will run
+ * as a global {@code ViewAssertion} ({@see ViewActions#addGlobalAssertion(ViewAssertion)}), and
+ * cover a variety of accessibility issues (see {@link AccessibilityCheckPreset#VIEW_CHECKS} and
+ * {@link AccessibilityCheckPreset#VIEW_HIERARCHY_CHECKS} to see which checks are run).
+ */
+public class AccessibilityCheckAssertion implements ViewAssertion {
+
+  private static final String TAG = "AccessibilityCheckAssertion";
+  private static final String SUPPRESS_ACCESSIBILITY_CHECKS_FLAG = "suppress_a11y_checks";
+  private boolean checksEnabled = false;
+  private boolean runChecksFromRootView = false;
+  private boolean throwExceptionForErrors = true;
+  private List<AccessibilityViewHierarchyCheck> viewHierarchyChecks =
+      new LinkedList<AccessibilityViewHierarchyCheck>();
+  private Matcher<? super AccessibilityViewCheckResult> suppressingMatcher = null;
+
+  public AccessibilityCheckAssertion() {
+    viewHierarchyChecks.addAll(AccessibilityCheckPreset.getViewChecksForPreset(
+        AccessibilityCheckPreset.LATEST));
+  }
+
+  @Override
+  public void check(View view, NoMatchingViewException noViewFoundException) {
+    if (noViewFoundException != null) {
+      Log.e(TAG,
+          String.format("'accessibility checks could not be performed because view '%s' was not"
+              + "found.\n", noViewFoundException.getViewMatcherDescription()));
+      throw noViewFoundException;
+    }
+    if (view == null) {
+      throw new NullPointerException();
+    }
+    checkAndReturnResults(view);
+  }
+
+  /**
+   * Runs accessibility checks and returns the list of results.
+   *
+   * @param view the {@link View} to check
+   * @return the resulting list of {@link AccessibilityViewCheckResult}
+   */
+  protected final List<AccessibilityViewCheckResult> checkAndReturnResults(View view) {
+    if (view != null) {
+      View viewToCheck = runChecksFromRootView ? view.getRootView() : view;
+      return runAccessibilityChecks(viewToCheck);
+    }
+    return Collections.<AccessibilityViewCheckResult>emptyList();
+  }
+
+  /**
+   * Enables accessibility checking as a global ViewAssertion in {@link ViewActions}.
+   * Check {@link #isEnabled()} before calling to avoid an {code IllegalStateException".}
+   *
+   * @throws {@code IllegalStateException} if accessibilty checks were already enabled
+   */
+  public void enable() {
+    if (checksEnabled) {
+      throw new IllegalStateException("Accessibility checks already enabled!");
+    }
+    checksEnabled = true;
+    ViewActions.addGlobalAssertion("Accessibility Checks", this);
+  }
+
+  /**
+   * Calls {@link #enable()} if a flag with the given key is present in the instrumentation
+   * arguments. These flags can be passed to {@code adb instrument} with the -e flag.
+   */
+  public void enableIfFlagPresent(String flag) {
+    Bundle args = InstrumentationArgumentsRegistry.getInstance();
+    String flagValue = args.getString(flag);
+    if (flagValue != null) {
+      enable();
+    }
+  }
+
+  /**
+   * Disables accessibility checking.
+   * Check {@link #isEnabled()} before calling to avoid an {code IllegalStateException".}
+   *
+   * @throws {@code IllegalStateException} if accessibilty checks were already disabled
+   */
+  public void disable() {
+    if (!checksEnabled) {
+      throw new IllegalStateException("Accessibility checks already disabled!");
+    }
+    checksEnabled = false;
+    ViewActions.removeGlobalAssertion(this);
+  }
+
+  /**
+   * @return true if accessibility checking is enabled
+   */
+  public boolean isEnabled() {
+    return checksEnabled;
+  }
+
+  /**
+   * @param runChecksFromRootView {@code true} to check all views in the hierarchy, {@code false} to
+   *        check only views in the hierarchy rooted at the passed in view. Default: {@code false}
+   * @return this
+   */
+  public AccessibilityCheckAssertion setRunChecksFromRootView(boolean runChecksFromRootView) {
+    this.runChecksFromRootView = runChecksFromRootView;
+    return this;
+  }
+
+  /**
+   * Suppresses all results that match the given matcher. Suppressed results will not be included
+   * in any logs or cause any {@code Exception} to be thrown
+   *
+   * @param resultMatcher a matcher to match a {@link AccessibilityViewCheckResult}
+   * @return this
+   */
+  public AccessibilityCheckAssertion setSuppressingResultMatcher(
+      Matcher<? super AccessibilityViewCheckResult> resultMatcher) {
+      suppressingMatcher = resultMatcher;
+    return this;
+  }
+
+  /**
+   * @param throwExceptionForErrors {@code true} to throw an {@code Exception} when there is at
+   *        least one error result, {@code false} to just log the error results to logcat.
+   *        Default: {@code true}
+   * @return this
+   */
+  public AccessibilityCheckAssertion setThrowExceptionForErrors(boolean throwExceptionForErrors) {
+    this.throwExceptionForErrors = throwExceptionForErrors;
+    return this;
+  }
+
+  private static boolean shouldCheckAccessibility() {
+    Bundle args = InstrumentationArgumentsRegistry.getInstance();
+    if (args != null && Boolean.valueOf(args.getString(SUPPRESS_ACCESSIBILITY_CHECKS_FLAG))) {
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * Runs accessibility checks on a {@code View} with the arguments passed through
+   * 'adb am shell instrument -e' (see {@link shouldCheckAccessibility()} to determine which
+   * arguments will allow these checks to run)
+   *
+   * @param view the {@code View} to run accessibility checks on
+   * @return a list of the results of the checks
+   */
+  private List<AccessibilityViewCheckResult> runAccessibilityChecks(
+      View view) {
+    if (!shouldCheckAccessibility()) {
+      return null;
+    }
+    List<AccessibilityViewCheckResult> results = new LinkedList<AccessibilityViewCheckResult>();
+    results.addAll(getViewHierarchyCheckResults(view, viewHierarchyChecks));
+    AccessibilityCheckResultUtils.suppressMatchingResults(results, suppressingMatcher);
+    processResults(results);
+    return results;
+  }
+
+  private static List<AccessibilityViewCheckResult> getViewHierarchyCheckResults(View root,
+      Iterable<AccessibilityViewHierarchyCheck> checks) {
+    List<AccessibilityViewCheckResult> results = new LinkedList<AccessibilityViewCheckResult>();
+    for (AccessibilityViewHierarchyCheck check : checks) {
+      results.addAll(check.runCheckOnViewHierarchy(root));
+    }
+    return results;
+  }
+
+  private void processResults(Iterable<AccessibilityViewCheckResult> results) {
+    if (results == null) {
+      return;
+    }
+    List<AccessibilityViewCheckResult> infos = AccessibilityCheckResultUtils.getResultsForType(
+        results, AccessibilityCheckResultType.INFO);
+    List<AccessibilityViewCheckResult> warnings = AccessibilityCheckResultUtils.getResultsForType(
+        results, AccessibilityCheckResultType.WARNING);
+    List<AccessibilityViewCheckResult> errors = AccessibilityCheckResultUtils.getResultsForType(
+        results, AccessibilityCheckResultType.ERROR);
+    for (AccessibilityViewCheckResult result : infos) {
+      Log.i(TAG, getResultMessage(result));
+    }
+    for (AccessibilityViewCheckResult result : warnings) {
+      Log.w(TAG, getResultMessage(result));
+    }
+    if (!errors.isEmpty() && throwExceptionForErrors) {
+      throw new EspressoAccessibilityException(errors);
+    } else {
+      for (AccessibilityViewCheckResult result : errors) {
+        Log.e(TAG, getResultMessage(result));
+      }
+    }
+  }
+
+  private static String getResultMessage(AccessibilityViewCheckResult result) {
+    StringBuilder message = new StringBuilder();
+    message.append(HumanReadables.describe(result.getView()));
+    message.append(": ");
+    message.append(result.getMessage());
+    return message.toString();
+  }
+
+  private static class EspressoAccessibilityException extends AccessibilityViewCheckException
+      implements EspressoException {
+    protected EspressoAccessibilityException(List<AccessibilityViewCheckResult> results) {
+      super(results);
+    }
+
+    @Override
+    protected String getResultMessage(AccessibilityViewCheckResult result) {
+      return AccessibilityCheckAssertion.getResultMessage(result);
+    }
+  }
+}