add validators for exmpted classes and scroll action

Change-Id: Iad22351b46df771e7a9f92edb9d84df44b5fe572
diff --git a/src/com/google/android/droiddriver/actions/ScrollAction.java b/src/com/google/android/droiddriver/actions/ScrollAction.java
new file mode 100644
index 0000000..6305c83
--- /dev/null
+++ b/src/com/google/android/droiddriver/actions/ScrollAction.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2013 DroidDriver committers
+ *
+ * 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.droiddriver.actions;
+
+/**
+ * Marker interface for a scroll action.
+ */
+public interface ScrollAction {
+}
diff --git a/src/com/google/android/droiddriver/actions/SwipeAction.java b/src/com/google/android/droiddriver/actions/SwipeAction.java
index a939536..6837741 100644
--- a/src/com/google/android/droiddriver/actions/SwipeAction.java
+++ b/src/com/google/android/droiddriver/actions/SwipeAction.java
@@ -30,7 +30,7 @@
 /**
  * An action that swipes the touch screen.
  */
-public class SwipeAction extends EventAction {
+public class SwipeAction extends EventAction implements ScrollAction {
   // Milliseconds between synthesized ACTION_MOVE events.
   // Note: ACTION_MOVE_INTERVAL is the minimum interval between injected events;
   // the actual interval typically is longer.
diff --git a/src/com/google/android/droiddriver/actions/accessibility/AccessibilityScrollAction.java b/src/com/google/android/droiddriver/actions/accessibility/AccessibilityScrollAction.java
index 88e7517..bdeddc8 100644
--- a/src/com/google/android/droiddriver/actions/accessibility/AccessibilityScrollAction.java
+++ b/src/com/google/android/droiddriver/actions/accessibility/AccessibilityScrollAction.java
@@ -19,13 +19,14 @@
 import android.view.accessibility.AccessibilityNodeInfo;
 
 import com.google.android.droiddriver.UiElement;
+import com.google.android.droiddriver.actions.ScrollAction;
 import com.google.android.droiddriver.scroll.Direction.PhysicalDirection;
 import com.google.android.droiddriver.util.Strings;
 
 /**
  * An {@link AccessibilityAction} that scrolls an UiElement.
  */
-public class AccessibilityScrollAction extends AccessibilityAction {
+public class AccessibilityScrollAction extends AccessibilityAction implements ScrollAction {
   private final PhysicalDirection direction;
 
   public AccessibilityScrollAction(PhysicalDirection direction) {
diff --git a/src/com/google/android/droiddriver/base/BaseUiElement.java b/src/com/google/android/droiddriver/base/BaseUiElement.java
index 3d74bdc..3122ba3 100644
--- a/src/com/google/android/droiddriver/base/BaseUiElement.java
+++ b/src/com/google/android/droiddriver/base/BaseUiElement.java
@@ -54,7 +54,7 @@
   public static final String ATTRIB_NOT_VISIBLE = "NotVisible";
 
   private UiElementActor uiElementActor = EventUiElementActor.INSTANCE;
-  private Validator[] validators = {};
+  private Validator validator = null;
 
   @SuppressWarnings("unchecked")
   @Override
@@ -163,13 +163,17 @@
   @Override
   public boolean perform(Action action) {
     Logs.call(this, "perform", action);
-    if (getParent() != null) {// don't check root
-      for (Validator validator : validators) {
-        if (!validator.isValid(this)) {
-          throw new DroidDriverException(validator + " failed");
-        }
+    if (validator != null && validator.isApplicable(this, action)) {
+      String failure = validator.validate(this, action);
+      if (failure != null) {
+        throw new DroidDriverException(toString() + " failed validation: " + failure);
       }
     }
+
+    // timeoutMillis <= 0 means no need to wait
+    if (action.getTimeoutMillis() <= 0) {
+      return doPerform(action);
+    }
     return performAndWait(action);
   }
 
@@ -180,11 +184,6 @@
   protected abstract void doPerformAndWait(FutureTask<Boolean> futureTask, long timeoutMillis);
 
   private boolean performAndWait(final Action action) {
-    // timeoutMillis <= 0 means no need to wait
-    if (action.getTimeoutMillis() <= 0) {
-      return doPerform(action);
-    }
-
     FutureTask<Boolean> futureTask = new FutureTask<Boolean>(new Callable<Boolean>() {
       @Override
       public Boolean call() {
@@ -192,6 +191,7 @@
       }
     });
     doPerformAndWait(futureTask, action.getTimeoutMillis());
+
     try {
       return futureTask.get();
     } catch (ExecutionException e) {
@@ -293,7 +293,10 @@
     this.uiElementActor = uiElementActor;
   }
 
-  public void setValidators(Validator[] validators) {
-    this.validators = validators;
+  /**
+   * Sets the validator to check when {@link #perform(Action)} is called.
+   */
+  public void setValidator(Validator validator) {
+    this.validator = validator;
   }
 }
diff --git a/src/com/google/android/droiddriver/scroll/AccessibilityEventScrollStepStrategy.java b/src/com/google/android/droiddriver/scroll/AccessibilityEventScrollStepStrategy.java
index 186bff8..f903746 100644
--- a/src/com/google/android/droiddriver/scroll/AccessibilityEventScrollStepStrategy.java
+++ b/src/com/google/android/droiddriver/scroll/AccessibilityEventScrollStepStrategy.java
@@ -188,11 +188,13 @@
 
   @Override
   public void doScroll(final UiElement container, final PhysicalDirection direction) {
-    // We do not call container.scroll(direction) because container.scroll
-    // internally calls UiAutomation.executeAndWaitForEvent which clears the
+    // We do not call container.scroll(direction) because it uses a SwipeAction
+    // with positive tTimeoutMillis. That path calls
+    // UiAutomation.executeAndWaitForEvent which clears the
     // AccessibilityEvent Queue, preventing us from fetching the last
     // accessibility event to determine if scrolling has finished.
-    SwipeAction.toScroll(direction).perform(container);
+    container
+        .perform(new SwipeAction(direction, SwipeAction.getScrollSteps(), false /* drag */, 0L/* timeoutMillis */));
   }
 
   /**
diff --git a/src/com/google/android/droiddriver/uiautomation/AccessibilityDriver.java b/src/com/google/android/droiddriver/uiautomation/AccessibilityDriver.java
index 63f6a59..6e7441d 100644
--- a/src/com/google/android/droiddriver/uiautomation/AccessibilityDriver.java
+++ b/src/com/google/android/droiddriver/uiautomation/AccessibilityDriver.java
@@ -20,13 +20,20 @@
 import android.view.accessibility.AccessibilityNodeInfo;
 
 import com.google.android.droiddriver.validators.DefaultAccessibilityValidator;
+import com.google.android.droiddriver.validators.ExemptRootValidator;
+import com.google.android.droiddriver.validators.FirstApplicableValidator;
+import com.google.android.droiddriver.validators.ExemptedClassesValidator;
+import com.google.android.droiddriver.validators.ExemptScrollActionValidator;
 import com.google.android.droiddriver.validators.Validator;
 
 /**
  * A UiAutomationDriver that validates accessibility.
  */
 public class AccessibilityDriver extends UiAutomationDriver {
-  private static final Validator[] VALIDATORS = {new DefaultAccessibilityValidator()};
+  private Validator validator = new FirstApplicableValidator(new ExemptRootValidator(),
+      new ExemptScrollActionValidator(), new ExemptedClassesValidator(),
+      // TODO: ImageViewValidator
+      new DefaultAccessibilityValidator());
 
   public AccessibilityDriver(Instrumentation instrumentation) {
     super(instrumentation);
@@ -36,7 +43,21 @@
   protected UiAutomationElement newUiElement(AccessibilityNodeInfo rawElement,
       UiAutomationElement parent) {
     UiAutomationElement newUiElement = super.newUiElement(rawElement, parent);
-    newUiElement.setValidators(VALIDATORS);
+    newUiElement.setValidator(validator);
     return newUiElement;
   }
+
+  /**
+   * Gets the current validator.
+   */
+  public Validator getValidator() {
+    return validator;
+  }
+
+  /**
+   * Sets the validator to check.
+   */
+  public void setValidator(Validator validator) {
+    this.validator = validator;
+  }
 }
diff --git a/src/com/google/android/droiddriver/validators/DefaultAccessibilityValidator.java b/src/com/google/android/droiddriver/validators/DefaultAccessibilityValidator.java
index e3c0778..b220a1b 100644
--- a/src/com/google/android/droiddriver/validators/DefaultAccessibilityValidator.java
+++ b/src/com/google/android/droiddriver/validators/DefaultAccessibilityValidator.java
@@ -19,20 +19,24 @@
 import android.text.TextUtils;
 
 import com.google.android.droiddriver.UiElement;
+import com.google.android.droiddriver.actions.Action;
 
 /**
- * Validates accessibility.
+ * Fall-back Validator for accessibility.
  */
-// TODO: Treats various types of UiElement as TalkBack does.
 public class DefaultAccessibilityValidator implements Validator {
   @Override
-  public boolean isValid(UiElement element) {
-    return !TextUtils.isEmpty(element.getContentDescription())
-        || !TextUtils.isEmpty(element.getText());
+  public boolean isApplicable(UiElement element, Action action) {
+    return true;
   }
 
   @Override
-  public String toString() {
-    return "Check non-empty c ontent description or text";
+  public String validate(UiElement element, Action action) {
+    return hasContentDescriptionOrText(element) ? null : "no content description or text";
+  }
+
+  private boolean hasContentDescriptionOrText(UiElement element) {
+    return !TextUtils.isEmpty(element.getContentDescription())
+        || !TextUtils.isEmpty(element.getText());
   }
 }
diff --git a/src/com/google/android/droiddriver/validators/ExemptRootValidator.java b/src/com/google/android/droiddriver/validators/ExemptRootValidator.java
new file mode 100644
index 0000000..1846e37
--- /dev/null
+++ b/src/com/google/android/droiddriver/validators/ExemptRootValidator.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2013 DroidDriver committers
+ *
+ * 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.droiddriver.validators;
+
+import com.google.android.droiddriver.UiElement;
+import com.google.android.droiddriver.actions.Action;
+
+/**
+ * Exempts root from validation.
+ */
+public class ExemptRootValidator implements Validator {
+  @Override
+  public boolean isApplicable(UiElement element, Action action) {
+    return element.getParent() == null; // don't check root
+  }
+
+  @Override
+  public String validate(UiElement element, Action action) {
+    return null;
+  }
+}
diff --git a/src/com/google/android/droiddriver/validators/ExemptScrollActionValidator.java b/src/com/google/android/droiddriver/validators/ExemptScrollActionValidator.java
new file mode 100644
index 0000000..d6829ed
--- /dev/null
+++ b/src/com/google/android/droiddriver/validators/ExemptScrollActionValidator.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2013 DroidDriver committers
+ *
+ * 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.droiddriver.validators;
+
+import com.google.android.droiddriver.UiElement;
+import com.google.android.droiddriver.actions.Action;
+import com.google.android.droiddriver.actions.ScrollAction;
+
+/**
+ * {@link ScrollAction} is not validated as TalkBack does not check the
+ * container.
+ */
+public class ExemptScrollActionValidator implements Validator {
+  @Override
+  public boolean isApplicable(UiElement element, Action action) {
+    return action instanceof ScrollAction;
+  }
+
+  @Override
+  public String validate(UiElement element, Action action) {
+    return null;
+  }
+}
diff --git a/src/com/google/android/droiddriver/validators/ExemptedClassesValidator.java b/src/com/google/android/droiddriver/validators/ExemptedClassesValidator.java
new file mode 100644
index 0000000..b04ffc5
--- /dev/null
+++ b/src/com/google/android/droiddriver/validators/ExemptedClassesValidator.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2013 DroidDriver committers
+ *
+ * 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.droiddriver.validators;
+
+import android.util.Log;
+
+import com.google.android.droiddriver.UiElement;
+import com.google.android.droiddriver.actions.Action;
+import com.google.android.droiddriver.util.Logs;
+
+/**
+ * Always validates the classes that TalkBack always has speech.
+ */
+public class ExemptedClassesValidator implements Validator {
+  private static final Class<?>[] EXEMPTED_CLASSES = {android.widget.Spinner.class,
+      android.widget.EditText.class, android.widget.SeekBar.class,
+      android.widget.AbsListView.class, android.widget.TabWidget.class};
+
+  @Override
+  public boolean isApplicable(UiElement element, Action action) {
+    String className = element.getClassName();
+    if (className == null || className.isEmpty()) {
+      return false;
+    }
+
+    Class<?> elementClass = null;
+    try {
+      elementClass = Class.forName(className);
+    } catch (ClassNotFoundException e) {
+      Logs.log(Log.WARN, e);
+      return false;
+    }
+
+    for (Class<?> clazz : EXEMPTED_CLASSES) {
+      if (clazz.isAssignableFrom(elementClass)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public String validate(UiElement element, Action action) {
+    return null;
+  }
+}
diff --git a/src/com/google/android/droiddriver/validators/FirstApplicableValidator.java b/src/com/google/android/droiddriver/validators/FirstApplicableValidator.java
new file mode 100644
index 0000000..1c89907
--- /dev/null
+++ b/src/com/google/android/droiddriver/validators/FirstApplicableValidator.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2013 DroidDriver committers
+ *
+ * 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.droiddriver.validators;
+
+import com.google.android.droiddriver.UiElement;
+import com.google.android.droiddriver.actions.Action;
+
+/**
+ * Iterates an array of validators and validates against the first one that is
+ * applicable. Note the order of validators matters.
+ */
+public class FirstApplicableValidator implements Validator {
+  private final Validator[] validators;
+
+  public FirstApplicableValidator(Validator... validators) {
+    this.validators = validators;
+  }
+
+  @Override
+  public boolean isApplicable(UiElement element, Action action) {
+    return true;
+  }
+
+  @Override
+  public String validate(UiElement element, Action action) {
+    for (Validator validator : validators) {
+      if (validator.isApplicable(element, action)) {
+        return validator.validate(element, action);
+      }
+    }
+    return "no applicable validator";
+  }
+}
diff --git a/src/com/google/android/droiddriver/validators/Validator.java b/src/com/google/android/droiddriver/validators/Validator.java
index f7812ed..4c0debb 100644
--- a/src/com/google/android/droiddriver/validators/Validator.java
+++ b/src/com/google/android/droiddriver/validators/Validator.java
@@ -17,6 +17,7 @@
 package com.google.android.droiddriver.validators;
 
 import com.google.android.droiddriver.UiElement;
+import com.google.android.droiddriver.actions.Action;
 
 /**
  * Interface for validating a UiElement, checked when an action is performed.
@@ -25,7 +26,14 @@
  */
 public interface Validator {
   /**
-   * Returns true if {@code element} is valid.
+   * Returns true if this {@link Validator} applies to {@code element} on this
+   * {@code action}.
    */
-  boolean isValid(UiElement element);
+  boolean isApplicable(UiElement element, Action action);
+
+  /**
+   * Returns {@code null} if {@code element} is valid on this {@code action},
+   * otherwise a string describing the failure.
+   */
+  String validate(UiElement element, Action action);
 }
diff --git a/src/com/google/android/droiddriver/validators/VisibilityValidator.java b/src/com/google/android/droiddriver/validators/VisibilityValidator.java
index 44570cf..df19bd7 100644
--- a/src/com/google/android/droiddriver/validators/VisibilityValidator.java
+++ b/src/com/google/android/droiddriver/validators/VisibilityValidator.java
@@ -17,13 +17,19 @@
 package com.google.android.droiddriver.validators;
 
 import com.google.android.droiddriver.UiElement;
+import com.google.android.droiddriver.actions.Action;
 
 /**
  * Validates visibility.
  */
 public class VisibilityValidator implements Validator {
   @Override
-  public boolean isValid(UiElement element) {
-    return element.isVisible();
+  public boolean isApplicable(UiElement element, Action action) {
+    return true;
+  }
+
+  @Override
+  public String validate(UiElement element, Action action) {
+    return element.isVisible() ? null : "invisible";
   }
 }