am 92df1a7c: Merge remote-tracking branch \'origin/kitkat-dev\'

* commit '92df1a7c65c0894a841e4ea42696c08516d8341a':
  Initial empty repository
diff --git a/.classpath b/.classpath
index f68b7e4..2386f0a 100644
--- a/.classpath
+++ b/.classpath
@@ -1,7 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <classpath>
 	<classpathentry kind="src" path="src"/>
-	<classpathentry kind="lib" path="libs/guava-13.0.jar"/>
 	<!--  TODO: clean up path before open sourcing -->
 	<classpathentry kind="lib" path="../../prebuilts/sdk/current/android.jar"/>
 	<classpathentry kind="output" path="bin/classes"/>
diff --git a/Android.mk b/Android.mk
index 826a81b..24affa6 100644
--- a/Android.mk
+++ b/Android.mk
@@ -3,9 +3,6 @@
 
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
-LOCAL_JAVA_LIBRARIES := \
-    libguava13
-
 LOCAL_MODULE := droiddriver
 LOCAL_MODULE_TAGS := optional
 LOCAL_SDK_VERSION := current
@@ -13,13 +10,3 @@
 include $(BUILD_STATIC_JAVA_LIBRARY)
 
 include $(CLEAR_VARS)
-
-LOCAL_MODULE_TAGS := optional
-
-LOCAL_PREBUILT_STATIC_JAVA_LIBRARIES := \
-    libguava13:libs/guava-13.0.jar
-
-include $(BUILD_MULTI_PREBUILT)
-
-include $(call all-makefiles-under,$(LOCAL_PATH))
-include $(call all-makefiles-under,$(LOCAL_PATH)/samples)
diff --git a/libs/guava-13.0.jar b/libs/guava-13.0.jar
deleted file mode 100644
index 67c2b4d..0000000
--- a/libs/guava-13.0.jar
+++ /dev/null
Binary files differ
diff --git a/samples/testapp/.classpath b/samples/testapp/.classpath
deleted file mode 100644
index 82214c4..0000000
--- a/samples/testapp/.classpath
+++ /dev/null
@@ -1,10 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<classpath>
-	<classpathentry kind="src" path="src"/>
-	<classpathentry kind="src" path="gen"/>
-	<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
-	<classpathentry kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
-	<classpathentry kind="lib" path="../../../../out/target/common/obj/JAVA_LIBRARIES/ActionBarSherlock_intermediates/javalib.jar"/>
-	<classpathentry kind="lib" path="../../libs/guava-13.0.jar"/>
-	<classpathentry kind="output" path="bin/classes"/>
-</classpath>
diff --git a/samples/testapp/.project b/samples/testapp/.project
deleted file mode 100644
index ad0703f..0000000
--- a/samples/testapp/.project
+++ /dev/null
@@ -1,33 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<projectDescription>
-	<name>testapp</name>
-	<comment></comment>
-	<projects>
-	</projects>
-	<buildSpec>
-		<buildCommand>
-			<name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
-			<arguments>
-			</arguments>
-		</buildCommand>
-		<buildCommand>
-			<name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
-			<arguments>
-			</arguments>
-		</buildCommand>
-		<buildCommand>
-			<name>org.eclipse.jdt.core.javabuilder</name>
-			<arguments>
-			</arguments>
-		</buildCommand>
-		<buildCommand>
-			<name>com.android.ide.eclipse.adt.ApkBuilder</name>
-			<arguments>
-			</arguments>
-		</buildCommand>
-	</buildSpec>
-	<natures>
-		<nature>com.android.ide.eclipse.adt.AndroidNature</nature>
-		<nature>org.eclipse.jdt.core.javanature</nature>
-	</natures>
-</projectDescription>
diff --git a/samples/testapp/Android.mk b/samples/testapp/Android.mk
deleted file mode 100644
index bba7f8c..0000000
--- a/samples/testapp/Android.mk
+++ /dev/null
@@ -1,21 +0,0 @@
-LOCAL_PATH := $(call my-dir)
-include $(CLEAR_VARS)
-
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
-LOCAL_RESOURCE_DIR := \
-    external/actionbarsherlock/library/res \
-    $(LOCAL_PATH)/res
-
-LOCAL_STATIC_JAVA_LIBRARIES := \
-    ActionBarSherlock \
-    libguava13
-
-LOCAL_AAPT_FLAGS += --auto-add-overlay
-
-LOCAL_PACKAGE_NAME := droiddriver.samples.testapp
-LOCAL_MODULE_TAGS := tests optional
-LOCAL_SDK_VERSION := 16
-
-include $(BUILD_PACKAGE)
-
-include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/samples/testapp/AndroidManifest.xml b/samples/testapp/AndroidManifest.xml
deleted file mode 100644
index 4bcfcad..0000000
--- a/samples/testapp/AndroidManifest.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.google.android.apps.common.testing.ui.testapp">
-
-    <uses-sdk android:minSdkVersion = "7" android:targetSdkVersion= "16"/>
-
-    <application android:label="UI Test App" android:icon="@drawable/ic_launcher" >
-        <activity android:name="MainActivity" android:label="UI Test App">
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN"/>
-                <category android:name="android.intent.category.LAUNCHER"/>
-            </intent-filter>
-        </activity>
-        <activity android:name="ActionBarActivity" android:label="actionbar activity"/>
-        <activity android:name="SendActivity" android:label="send activity"/>
-        <activity android:name="DisplayActivity" android:label="display activity"/>
-        <activity android:name="GestureActivity" android:label="gesture activity" android:exported="true"/>
-        <activity android:name="ScrollActivity" android:label="scroll activity" android:exported="true"/>
-        <activity android:name="LongListActivity" android:label="list activity" android:exported="true"/>
-        <activity android:name="MenuActivity" android:label="menu activity"/>
-    </application>
-
-    <uses-permission android:name="android.permission.CALL_PHONE"></uses-permission>
-</manifest>
diff --git a/samples/testapp/README.android b/samples/testapp/README.android
deleted file mode 100644
index f52b93c..0000000
--- a/samples/testapp/README.android
+++ /dev/null
@@ -1,6 +0,0 @@
-URL: //google3/java/com/google/android/apps/common/testing/ui/testapp
-Description: "google3 android testing demo testapp"
-
-DO NOT OPEN SOURCE
-For internal demo only
-
diff --git a/samples/testapp/project.properties b/samples/testapp/project.properties
deleted file mode 100644
index 9b84a6b..0000000
--- a/samples/testapp/project.properties
+++ /dev/null
@@ -1,14 +0,0 @@
-# This file is automatically generated by Android Tools.
-# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
-#
-# This file must be checked in Version Control Systems.
-#
-# To customize properties used by the Ant build system edit
-# "ant.properties", and override values to adapt the script to your
-# project structure.
-#
-# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
-#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
-
-# Project target.
-target=android-16
diff --git a/samples/testapp/res/drawable-hdpi/ic_action_calendar.png b/samples/testapp/res/drawable-hdpi/ic_action_calendar.png
deleted file mode 100644
index c7bd88b..0000000
--- a/samples/testapp/res/drawable-hdpi/ic_action_calendar.png
+++ /dev/null
Binary files differ
diff --git a/samples/testapp/res/drawable-hdpi/ic_action_key.png b/samples/testapp/res/drawable-hdpi/ic_action_key.png
deleted file mode 100644
index ff876a0..0000000
--- a/samples/testapp/res/drawable-hdpi/ic_action_key.png
+++ /dev/null
Binary files differ
diff --git a/samples/testapp/res/drawable-hdpi/ic_action_lock.png b/samples/testapp/res/drawable-hdpi/ic_action_lock.png
deleted file mode 100644
index 1c80686..0000000
--- a/samples/testapp/res/drawable-hdpi/ic_action_lock.png
+++ /dev/null
Binary files differ
diff --git a/samples/testapp/res/drawable-hdpi/ic_action_save.png b/samples/testapp/res/drawable-hdpi/ic_action_save.png
deleted file mode 100644
index 827355d..0000000
--- a/samples/testapp/res/drawable-hdpi/ic_action_save.png
+++ /dev/null
Binary files differ
diff --git a/samples/testapp/res/drawable-hdpi/ic_action_search.png b/samples/testapp/res/drawable-hdpi/ic_action_search.png
deleted file mode 100644
index b826566..0000000
--- a/samples/testapp/res/drawable-hdpi/ic_action_search.png
+++ /dev/null
Binary files differ
diff --git a/samples/testapp/res/drawable-hdpi/ic_action_world.png b/samples/testapp/res/drawable-hdpi/ic_action_world.png
deleted file mode 100644
index 612a5f2..0000000
--- a/samples/testapp/res/drawable-hdpi/ic_action_world.png
+++ /dev/null
Binary files differ
diff --git a/samples/testapp/res/drawable-hdpi/ic_launcher.png b/samples/testapp/res/drawable-hdpi/ic_launcher.png
deleted file mode 100644
index c1615e0..0000000
--- a/samples/testapp/res/drawable-hdpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/samples/testapp/res/drawable-ldpi/ic_action_calendar.png b/samples/testapp/res/drawable-ldpi/ic_action_calendar.png
deleted file mode 100644
index 37a9d7a..0000000
--- a/samples/testapp/res/drawable-ldpi/ic_action_calendar.png
+++ /dev/null
Binary files differ
diff --git a/samples/testapp/res/drawable-ldpi/ic_action_key.png b/samples/testapp/res/drawable-ldpi/ic_action_key.png
deleted file mode 100644
index 8628a15..0000000
--- a/samples/testapp/res/drawable-ldpi/ic_action_key.png
+++ /dev/null
Binary files differ
diff --git a/samples/testapp/res/drawable-ldpi/ic_action_lock.png b/samples/testapp/res/drawable-ldpi/ic_action_lock.png
deleted file mode 100644
index a29abbb..0000000
--- a/samples/testapp/res/drawable-ldpi/ic_action_lock.png
+++ /dev/null
Binary files differ
diff --git a/samples/testapp/res/drawable-ldpi/ic_action_save.png b/samples/testapp/res/drawable-ldpi/ic_action_save.png
deleted file mode 100644
index a51c100..0000000
--- a/samples/testapp/res/drawable-ldpi/ic_action_save.png
+++ /dev/null
Binary files differ
diff --git a/samples/testapp/res/drawable-ldpi/ic_action_search.png b/samples/testapp/res/drawable-ldpi/ic_action_search.png
deleted file mode 100644
index 1b4aac6..0000000
--- a/samples/testapp/res/drawable-ldpi/ic_action_search.png
+++ /dev/null
Binary files differ
diff --git a/samples/testapp/res/drawable-ldpi/ic_action_world.png b/samples/testapp/res/drawable-ldpi/ic_action_world.png
deleted file mode 100644
index c27143c..0000000
--- a/samples/testapp/res/drawable-ldpi/ic_action_world.png
+++ /dev/null
Binary files differ
diff --git a/samples/testapp/res/drawable-ldpi/ic_launcher.png b/samples/testapp/res/drawable-ldpi/ic_launcher.png
deleted file mode 100644
index 110987a..0000000
--- a/samples/testapp/res/drawable-ldpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/samples/testapp/res/drawable-mdpi/ic_action_calendar.png b/samples/testapp/res/drawable-mdpi/ic_action_calendar.png
deleted file mode 100644
index 7cb5f27..0000000
--- a/samples/testapp/res/drawable-mdpi/ic_action_calendar.png
+++ /dev/null
Binary files differ
diff --git a/samples/testapp/res/drawable-mdpi/ic_action_key.png b/samples/testapp/res/drawable-mdpi/ic_action_key.png
deleted file mode 100644
index 6bb6ff3..0000000
--- a/samples/testapp/res/drawable-mdpi/ic_action_key.png
+++ /dev/null
Binary files differ
diff --git a/samples/testapp/res/drawable-mdpi/ic_action_lock.png b/samples/testapp/res/drawable-mdpi/ic_action_lock.png
deleted file mode 100644
index ee9dea0..0000000
--- a/samples/testapp/res/drawable-mdpi/ic_action_lock.png
+++ /dev/null
Binary files differ
diff --git a/samples/testapp/res/drawable-mdpi/ic_action_save.png b/samples/testapp/res/drawable-mdpi/ic_action_save.png
deleted file mode 100644
index eebcf21..0000000
--- a/samples/testapp/res/drawable-mdpi/ic_action_save.png
+++ /dev/null
Binary files differ
diff --git a/samples/testapp/res/drawable-mdpi/ic_action_search.png b/samples/testapp/res/drawable-mdpi/ic_action_search.png
deleted file mode 100644
index 3516c9e..0000000
--- a/samples/testapp/res/drawable-mdpi/ic_action_search.png
+++ /dev/null
Binary files differ
diff --git a/samples/testapp/res/drawable-mdpi/ic_action_world.png b/samples/testapp/res/drawable-mdpi/ic_action_world.png
deleted file mode 100644
index f7e8dcf..0000000
--- a/samples/testapp/res/drawable-mdpi/ic_action_world.png
+++ /dev/null
Binary files differ
diff --git a/samples/testapp/res/drawable-mdpi/ic_launcher.png b/samples/testapp/res/drawable-mdpi/ic_launcher.png
deleted file mode 100644
index 90f091c..0000000
--- a/samples/testapp/res/drawable-mdpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/samples/testapp/res/drawable-xhdpi/ic_action_calendar.png b/samples/testapp/res/drawable-xhdpi/ic_action_calendar.png
deleted file mode 100644
index d34b110..0000000
--- a/samples/testapp/res/drawable-xhdpi/ic_action_calendar.png
+++ /dev/null
Binary files differ
diff --git a/samples/testapp/res/drawable-xhdpi/ic_action_key.png b/samples/testapp/res/drawable-xhdpi/ic_action_key.png
deleted file mode 100644
index 31c6756..0000000
--- a/samples/testapp/res/drawable-xhdpi/ic_action_key.png
+++ /dev/null
Binary files differ
diff --git a/samples/testapp/res/drawable-xhdpi/ic_action_lock.png b/samples/testapp/res/drawable-xhdpi/ic_action_lock.png
deleted file mode 100644
index 63797e2..0000000
--- a/samples/testapp/res/drawable-xhdpi/ic_action_lock.png
+++ /dev/null
Binary files differ
diff --git a/samples/testapp/res/drawable-xhdpi/ic_action_save.png b/samples/testapp/res/drawable-xhdpi/ic_action_save.png
deleted file mode 100644
index 2e7c579..0000000
--- a/samples/testapp/res/drawable-xhdpi/ic_action_save.png
+++ /dev/null
Binary files differ
diff --git a/samples/testapp/res/drawable-xhdpi/ic_action_search.png b/samples/testapp/res/drawable-xhdpi/ic_action_search.png
deleted file mode 100644
index 3539eab..0000000
--- a/samples/testapp/res/drawable-xhdpi/ic_action_search.png
+++ /dev/null
Binary files differ
diff --git a/samples/testapp/res/drawable-xhdpi/ic_action_world.png b/samples/testapp/res/drawable-xhdpi/ic_action_world.png
deleted file mode 100644
index e1b21d3..0000000
--- a/samples/testapp/res/drawable-xhdpi/ic_action_world.png
+++ /dev/null
Binary files differ
diff --git a/samples/testapp/res/drawable-xhdpi/ic_launcher.png b/samples/testapp/res/drawable-xhdpi/ic_launcher.png
deleted file mode 100644
index 0f6604b..0000000
--- a/samples/testapp/res/drawable-xhdpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/samples/testapp/res/layout/actionbar_activity.xml b/samples/testapp/res/layout/actionbar_activity.xml
deleted file mode 100644
index 9d63cff..0000000
--- a/samples/testapp/res/layout/actionbar_activity.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="fill_parent"
-    android:layout_height="fill_parent"
-    android:orientation="vertical"
-    android:padding="20dip" >
-    <LinearLayout
-        android:layout_width="fill_parent"
-        android:layout_height="wrap_content"
-        android:gravity="center_horizontal"
-        android:orientation="horizontal" >
-        <Button
-            android:id="@+id/show"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_marginRight="10dp"
-            android:text="@string/text_show" />
-        <Button
-            android:id="@+id/hide"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:text="@string/text_hide" />
-    </LinearLayout>
-    <TextView
-        android:id="@+id/textActionBarResult"
-        android:layout_width="fill_parent"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="20dp"
-        android:gravity="center"
-        android:text="@string/text_empty"
-        android:textAppearance="?android:attr/textAppearanceSmall" />
-</LinearLayout>
\ No newline at end of file
diff --git a/samples/testapp/res/layout/display_activity.xml b/samples/testapp/res/layout/display_activity.xml
deleted file mode 100644
index 1d8a0da..0000000
--- a/samples/testapp/res/layout/display_activity.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  XML for screen for displaying data received from another activity.
--->
-<LinearLayout
-  xmlns:android="http://schemas.android.com/apk/res/android"
-  android:layout_width="wrap_content"
-  android:layout_height="wrap_content"
-  android:layout_alignParentRight="true"
-  android:layout_alignParentTop="true"
-  android:orientation="vertical" >
-
-    <TextView
-        android:id="@+id/displayTitle"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:text="@string/display_title"
-        android:layout_marginTop="10dp"
-        android:textAppearance="?android:attr/textAppearanceMedium" />
-
-    <TextView
-        android:id="@+id/displayData"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="10dp"
-        android:text="@string/display_data"
-        android:textAppearance="?android:attr/textAppearanceSmall" />
-
-</LinearLayout>
\ No newline at end of file
diff --git a/samples/testapp/res/layout/gesture_activity.xml b/samples/testapp/res/layout/gesture_activity.xml
deleted file mode 100644
index e778f3d..0000000
--- a/samples/testapp/res/layout/gesture_activity.xml
+++ /dev/null
@@ -1,67 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  XML for screen providing ability to test different clicks and gestures.
--->
-
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="fill_parent"
-    android:layout_height="fill_parent"
-    android:fillViewport="true"
-    android:orientation="vertical" >
-
-    <LinearLayout
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content" >
-
-        <TextView
-            android:id="@+id/textClick"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_weight="1"
-            android:gravity="center"
-            android:text="@string/text_click"
-            android:textAppearance="?android:attr/textAppearanceMedium"
-            android:visibility="gone" />
-
-        <TextView
-            android:id="@+id/textLongClick"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_weight="1"
-            android:gravity="center"
-            android:text="@string/text_longClick"
-            android:textAppearance="?android:attr/textAppearanceMedium"
-            android:visibility="gone" />
-
-        <TextView
-            android:id="@+id/textSwipe"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_weight="1"
-            android:gravity="center"
-            android:text="@string/text_swipe"
-            android:textAppearance="?android:attr/textAppearanceMedium"
-            android:visibility="gone" />
-
-        <TextView
-            android:id="@+id/textDoubleClick"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_weight="1"
-            android:gravity="center"
-            android:text="@string/text_doubleClick"
-            android:textAppearance="?android:attr/textAppearanceMedium"
-            android:visibility="gone" />
-    </LinearLayout>
-
-    <View
-      android:id="@+id/gestureArea"
-      android:layout_width="fill_parent"
-      android:layout_height="0dp"
-      android:layout_weight="1"
-      android:gravity="top"
-      android:clickable="true"
-      android:onClick="areaClicked"
-      />
-
-</LinearLayout>
diff --git a/samples/testapp/res/layout/list_activity.xml b/samples/testapp/res/layout/list_activity.xml
deleted file mode 100644
index 813c756..0000000
--- a/samples/testapp/res/layout/list_activity.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  XML for a screen with a list view.
--->
-
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:orientation="vertical" >
-
-    <TextView
-      android:id="@+id/selection_pos"
-      android:layout_width="match_parent"
-      android:layout_height="wrap_content" >
-    </TextView>
-
-    <TextView
-      android:id="@+id/selection_class"
-      android:layout_width="match_parent"
-      android:layout_height="wrap_content" >
-    </TextView>
-
-    <ListView
-        android:id="@+id/list"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content" >
-    </ListView>
-
-</LinearLayout>
diff --git a/samples/testapp/res/layout/list_item.xml b/samples/testapp/res/layout/list_item.xml
deleted file mode 100644
index 03c21ca..0000000
--- a/samples/testapp/res/layout/list_item.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="fill_parent"
-    android:layout_height="fill_parent"
-    >
-
-    <TextView
-      android:id="@+id/item_content"
-      android:layout_width="wrap_content"
-      android:layout_height="wrap_content"/>
-    <TextView
-      android:id="@+id/item_size"
-      android:layout_width="wrap_content"
-      android:layout_height="wrap_content"
-      android:layout_marginLeft="50dp"/>
-</LinearLayout>
diff --git a/samples/testapp/res/layout/menu_activity.xml b/samples/testapp/res/layout/menu_activity.xml
deleted file mode 100644
index 8e8c36f..0000000
--- a/samples/testapp/res/layout/menu_activity.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="fill_parent"
-    android:layout_height="fill_parent"
-    android:orientation="vertical"
-    android:padding="20dip" >
-    <LinearLayout
-        android:layout_width="fill_parent"
-        android:layout_height="wrap_content"
-        android:gravity="center_horizontal"
-        android:orientation="horizontal" >
-        <Button
-            android:id="@+id/popupButton"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_marginRight="10dp"
-            android:text="Click here for popup menu!"
-            android:onClick="showPopup" />
-    </LinearLayout>
-    <LinearLayout
-        android:layout_width="fill_parent"
-        android:layout_height="wrap_content"
-        android:gravity="center_horizontal"
-        android:orientation="horizontal" >
-        <TextView
-            android:id="@+id/textContextMenu"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:text="long-click here for context menu!"
-            android:textAppearance="?android:attr/textAppearanceMedium" />
-    </LinearLayout>
-    <TextView
-        android:id="@+id/textMenuResult"
-        android:layout_width="fill_parent"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="20dp"
-        android:gravity="center"
-        android:text="@string/text_empty"
-        android:textAppearance="?android:attr/textAppearanceSmall" />
-</LinearLayout>
\ No newline at end of file
diff --git a/samples/testapp/res/layout/popup_window.xml b/samples/testapp/res/layout/popup_window.xml
deleted file mode 100644
index 73e70b1..0000000
--- a/samples/testapp/res/layout/popup_window.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  XML for screen providing ability to enter text and send some intents.
--->
-
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content"
-    android:orientation="vertical" >
-
-  <TextView
-      android:id="@+id/popup_title"
-      android:layout_width="wrap_content"
-      android:layout_height="wrap_content"
-      android:layout_marginTop="10dp"
-      android:text="@string/popup_title"
-      android:textAppearance="?android:attr/textAppearanceLarge"/>
-
-  <TextView
-      android:layout_width="wrap_content"
-      android:layout_height="wrap_content"
-      android:layout_marginTop="10dp"
-      android:text="@string/popup_window_text" />
-
-</LinearLayout>
diff --git a/samples/testapp/res/layout/scroll_activity.xml b/samples/testapp/res/layout/scroll_activity.xml
deleted file mode 100644
index db7f270..0000000
--- a/samples/testapp/res/layout/scroll_activity.xml
+++ /dev/null
@@ -1,73 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  XML for screen that holds various scroll views.
--->
-
-<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="fill_parent"
-    android:layout_height="fill_parent"
-    android:fillViewport="true"
-    android:orientation="vertical" >
-
-  <LinearLayout
-      android:layout_width="fill_parent"
-      android:layout_height="wrap_content"
-      android:orientation="vertical" >
-
-    <TextView
-        android:id="@+id/topLeft"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="10dp"
-        android:text="top_left" />
-
-    <ScrollView
-        android:layout_width="fill_parent"
-        android:layout_height="50dp"
-        android:layout_marginTop="10dp"
-        android:background="#FFDDDDDD"
-        android:fillViewport="true"
-        android:orientation="vertical" >
-      <LinearLayout
-          android:layout_width="wrap_content"
-          android:layout_height="wrap_content"
-          android:orientation="vertical" >
-        <TextView
-            android:id="@+id/doubleScroll"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:textColor="#000000"
-            android:layout_marginTop="200dp"
-            android:text="double_scroll" />
-      </LinearLayout>
-    </ScrollView>
-
-    <!-- Keep this on bottom to test scrolling to views that are not showing. -->
-    <!-- Huge top margin to guarantee this being out of view on large screen layout. -->
-    <HorizontalScrollView
-        android:layout_width="fill_parent"
-        android:layout_height="50dp"
-        android:background="#FFDDDDDD"
-        android:layout_marginTop="3000dp">
-      <LinearLayout
-          android:layout_width="wrap_content"
-          android:layout_height="wrap_content"
-          android:orientation="horizontal" >
-        <TextView
-            android:id="@+id/bottomLeft"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:textColor="#000000"
-            android:text="bottom_left" />
-        <TextView
-            android:id="@+id/bottomRight"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_marginLeft="3000dp"
-            android:textColor="#000000"
-            android:text="bottom_right" />
-      </LinearLayout>
-    </HorizontalScrollView>
-
-  </LinearLayout>
-</ScrollView>
diff --git a/samples/testapp/res/layout/send_activity.xml b/samples/testapp/res/layout/send_activity.xml
deleted file mode 100644
index e1fa21e..0000000
--- a/samples/testapp/res/layout/send_activity.xml
+++ /dev/null
@@ -1,257 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  XML for screen providing ability to enter text, send some intents,
-  switch to gesture activity and test scroll down action.
--->
-
-<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="fill_parent"
-    android:layout_height="fill_parent"
-    android:fillViewport="true"
-    android:orientation="vertical" >
-
-  <LinearLayout
-      android:layout_width="wrap_content"
-      android:layout_height="wrap_content"
-      android:orientation="vertical" >
-
-    <TextView
-        android:id="@+id/sendTitle"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="10dp"
-        android:text="@string/send_title"
-        android:textAppearance="?android:attr/textAppearanceLarge"
-        android:onClick="sendData"
-        android:clickable="true" />
-
-    <EditText
-        android:id="@+id/sendDataEditText"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="10dp"
-        android:ems="10"
-        android:hint="@string/send_hint" />
-
-    <Button
-        android:id="@+id/sendButton"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="10dp"
-        android:text="@string/button_send"
-        android:onClick="sendData" />
-
-    <EditText
-        android:id="@+id/enterDataEditText"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="10dp"
-        android:ems="10"
-        android:hint="@string/enter_hint" />
-
-    <TextView
-        android:id="@+id/enterDataResponseText"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="10dp"
-        android:text=""
-        android:textAppearance="?android:attr/textAppearanceLarge" />
-
-    <TextView
-        android:id="@+id/call"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="10dp"
-        android:text="@string/send_intent_to_call"
-        android:textAppearance="?android:attr/textAppearanceLarge" />
-
-    <EditText
-        android:id="@+id/sendDataToCallEditText"
-        android:layout_width="229dp"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="10dp"
-        android:ems="10"
-        android:hint="@string/send_hint_for_call" />
-
-    <Button
-        android:id="@+id/sendToCallButton"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="10dp"
-        android:text="@string/button_call"
-        android:onClick="sendDataToCall" />
-
-
-
-    <TextView
-        android:id="@+id/sendDataMessage"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="10dp"
-        android:text="@string/send_message"
-        android:textAppearance="?android:attr/textAppearanceLarge" />
-
-    <EditText
-        android:id="@+id/sendDataToMessageEditText"
-        android:layout_width="290dp"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="10dp"
-        android:ems="10"
-        android:hint="@string/send_hint"
-        android:text="@string/send_data_to_message_edit_text" />
-
-    <Button
-        android:id="@+id/sendMessageButton"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="10dp"
-        android:text="@string/button_to_message"
-        android:onClick="sendMessage" />
-
-    <TextView
-        android:id="@+id/gotoBrowser"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="10dp"
-        android:text="@string/send_intent_to_browser"
-        android:textAppearance="?android:attr/textAppearanceLarge" />
-
-    <EditText
-        android:id="@+id/sendDataToBrowserEditText"
-        android:layout_width="290dp"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="10dp"
-        android:ems="10"
-        android:hint="@string/send_hint" />
-
-    <Button
-        android:id="@+id/sendToBrowserButton"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="10dp"
-        android:text="@string/button_to_browser"
-        android:onClick="sendDataToBrowser" />
-
-    <TextView
-        android:id="@+id/pickContactTitle"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="10dp"
-        android:text="@string/pick_title"
-        android:textAppearance="?android:attr/textAppearanceLarge" />
-
-    <Button
-        android:id="@+id/pickButton"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="10dp"
-        android:text="@string/button_pick"
-        android:onClick="pickContact" />
-
-    <TextView
-        android:id="@+id/phoneNumber"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="10dp" />
-
-    <TextView
-        android:id="@+id/market"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="10dp"
-        android:text="@string/send_intent_to_market"
-        android:textAppearance="?android:attr/textAppearanceLarge" />
-
-    <EditText
-        android:id="@+id/sendToMarketData"
-        android:layout_width="229dp"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="10dp"
-        android:ems="10"
-        android:hint="@string/send_hint_to_market" />
-
-    <Button
-        android:id="@+id/sendToMarketButton"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="10dp"
-        android:text="@string/button_market"
-        android:onClick="clickToMarket" />
-
-    <TextView
-        android:id="@+id/gestureTitle"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="10dp"
-        android:text="@string/gesture_title"
-        android:textAppearance="?android:attr/textAppearanceLarge" />
-
-    <Button
-        android:id="@+id/goToGestureActivity"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="10dp"
-        android:text="@string/button_gesture"
-        android:onClick="clickToGesture" />
-
-    <Button
-        android:id="@+id/scrollButton"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="10dp"
-        android:text="@string/launch_scroll_activity"
-        android:onClick="clickToScroll" />
-
-     <Button
-        android:id="@+id/listButton"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="10dp"
-        android:text="@string/launch_list_activity"
-        android:onClick="clickToList" />
-
-    <Button
-        android:id="@+id/makeAlertDialog"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="10dp"
-        android:text="@string/make_alert_dialog_button"
-        android:onClick="showDialog" />
-
-    <Button
-        android:id="@+id/makePopupMenuButton"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="10dp"
-        android:text="@string/make_popup_menu_button"
-        android:onClick="showPopupMenu" />
-
-    <Button
-        android:id="@+id/makePopupViewButton"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="10dp"
-        android:text="@string/make_popup_view_button"
-        android:onClick="showPopupView" />
-
-    <!-- Keep this on bottom to test scrolling to views that are not showing. -->
-    <!-- Huge top margin to guarantee this being out of view on large screen layout. -->
-    <Button
-        android:id="@+id/bottomSendButton"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="1000dp"
-        android:text="@string/button_send_bottom"
-        android:onClick="sendData" />
-
-    <TextView
-        android:id="@+id/bottomSendTextView"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="1000dp"
-        android:text="@string/send_title"
-        android:onClick="sendData"
-        android:clickable="true" />
-
-  </LinearLayout>
-</ScrollView>
diff --git a/samples/testapp/res/menu/contextmenu.xml b/samples/testapp/res/menu/contextmenu.xml
deleted file mode 100644
index bbf4ff0..0000000
--- a/samples/testapp/res/menu/contextmenu.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<menu xmlns:android="http://schemas.android.com/apk/res/android" >
-    <item android:id="@+id/context_item1" android:title="@string/context_item_1_text"></item>
-    <item android:id="@+id/context_item2" android:title="@string/context_item_2_text"></item>
-    <item android:id="@+id/context_item3" android:title="@string/context_item_3_text"></item>
-</menu>
\ No newline at end of file
diff --git a/samples/testapp/res/menu/optionsmenu.xml b/samples/testapp/res/menu/optionsmenu.xml
deleted file mode 100644
index 4b044b1..0000000
--- a/samples/testapp/res/menu/optionsmenu.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<menu xmlns:android="http://schemas.android.com/apk/res/android" >
-    <item android:id="@+id/option_item1" android:title="@string/options_item_1_text"></item>
-    <item android:id="@+id/option_item2" android:title="@string/options_item_2_text"></item>
-    <item android:id="@+id/option_item3" android:title="@string/options_item_3_text"></item>
-</menu>
\ No newline at end of file
diff --git a/samples/testapp/res/menu/popup_menu.xml b/samples/testapp/res/menu/popup_menu.xml
deleted file mode 100644
index 1ff277d..0000000
--- a/samples/testapp/res/menu/popup_menu.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-
-<menu xmlns:android="http://schemas.android.com/apk/res/android">
-  <item android:id="@+id/menu_item_1"
-    android:title="@string/item_1_text"/>
-  <item android:id="@+id/menu_item_2"
-    android:title="@string/item_2_text" />
-</menu>
diff --git a/samples/testapp/res/menu/popupmenu.xml b/samples/testapp/res/menu/popupmenu.xml
deleted file mode 100644
index 2f46742..0000000
--- a/samples/testapp/res/menu/popupmenu.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<menu xmlns:android="http://schemas.android.com/apk/res/android" >
-    <item android:id="@+id/popup_item1" android:title="@string/popup_item_1_text"></item>
-    <item android:id="@+id/popup_item2" android:title="@string/popup_item_2_text"></item>
-    <item android:id="@+id/popup_item3" android:title="@string/popup_item_3_text"></item>
-</menu>
\ No newline at end of file
diff --git a/samples/testapp/res/values/strings.xml b/samples/testapp/res/values/strings.xml
deleted file mode 100644
index 1467a70..0000000
--- a/samples/testapp/res/values/strings.xml
+++ /dev/null
@@ -1,54 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources>
-    <string name="button_send">Send</string>
-    <string name="button_send_bottom">Send Bottom</string>
-    <string name="button_call">Call</string>
-    <string name="button_to_message">SMS</string>
-    <string name="button_to_browser">Go To Browser</string>
-    <string name="button_pick">Pick</string>
-    <string name="button_market">Goto Market</string>
-    <string name="button_gesture">Go To Gesture Activity</string>
-    <string name="display_data" />
-    <string name="display_title">Data from sender</string>
-    <string name="dialog_title">An emergency alert</string>
-    <string name="dialog_message">A really important message</string>
-    <string name="enter_hint">Type text and press Enter</string>
-    <string name="launch_list_activity">Launch list_activity</string>
-    <string name="launch_scroll_activity">Launch scroll_activity</string>
-    <string name="send_data_to_message_edit_text">send_data_to_message_edit_text</string>
-    <string name="send_hint">Enter text here</string>
-    <string name="send_hint_for_call">Enter number here</string>
-    <string name="send_title">Send internal intent with data</string>
-    <string name="send_intent_to_call">Enter Number To Call</string>
-    <string name="send_intent_to_market">Enter App Id From Market</string>
-    <string name="send_intent_to_browser">Enter URL you wanted to go.</string>
-    <string name="send_hint_to_market">Enter App id</string>
-    <string name="send_message">Enter Message</string>
-    <string name="pick_title">Pick a Contact</string>
-    <string name="item_1_text">Menu Item 1</string>
-    <string name="item_2_text">Goodbye</string>
-    <string name="make_alert_dialog_button">Make an alert dialog</string>
-    <string name="make_popup_menu_button">Make a Popup Window</string>
-    <string name="make_popup_view_button">Make a Popup Window</string>
-    <string name="popup_window_text">I am in a popup window</string>
-    <string name="popup_title">A popup window</string>
-    <string name="gesture_title">Show Gesture Activity</string>
-    <string name="text_click">Click</string>
-    <string name="text_longClick">Long Click</string>
-    <string name="text_swipe">Swipe</string>
-    <string name="text_doubleClick">Double Click</string>
-    <string name="text_empty"></string>
-    <string name="text_show">Show</string>
-    <string name="text_hide">Hide</string>
-    <string name="popup_item_1_text">Popup Item 1</string>
-    <string name="popup_item_2_text">Popup Item 2</string>
-    <string name="popup_item_3_text">Popup Item 3</string>
-    <string name="context_item_1_text">Context Item 1</string>
-    <string name="context_item_2_text">Context Item 2</string>
-    <string name="context_item_3_text">Context Item 3</string>
-    <string name="options_item_1_text">Options Item 1</string>
-    <string name="options_item_2_text">Options Item 2</string>
-    <string name="options_item_3_text">Options Item 3</string>
-    
-    
-</resources>
\ No newline at end of file
diff --git a/samples/testapp/src/com/google/android/apps/common/testing/ui/testapp/ActionBarActivity.java b/samples/testapp/src/com/google/android/apps/common/testing/ui/testapp/ActionBarActivity.java
deleted file mode 100644
index 9d17991..0000000
--- a/samples/testapp/src/com/google/android/apps/common/testing/ui/testapp/ActionBarActivity.java
+++ /dev/null
@@ -1,100 +0,0 @@
-package com.google.android.apps.common.testing.ui.testapp;
-
-import android.os.Bundle;
-import android.view.View;
-import android.widget.Button;
-import android.widget.TextView;
-
-import com.actionbarsherlock.app.SherlockActivity;
-import com.actionbarsherlock.view.ActionMode;
-import com.actionbarsherlock.view.Menu;
-import com.actionbarsherlock.view.MenuItem;
-
-/**
- * Shows ActionBar with a lot of items to get Action overflow on large displays. Click on item
- * changes text of R.id.textActionBarResult. We have to use third-party ActionBarSherlock, because
- * Android Action is available since API 11 and we support API >= 7.
- */
-public class ActionBarActivity extends SherlockActivity {
-  private ActionMode mode;
-
-  @Override
-  protected void onCreate(Bundle savedInstanceState) {
-    setTheme(R.style.Theme_Sherlock);
-    super.onCreate(savedInstanceState);
-    setContentView(R.layout.actionbar_activity);
-
-    mode = startActionMode(new TestActionMode());
-
-    ((Button) findViewById(R.id.show)).setOnClickListener(new View.OnClickListener() {
-      @Override
-      public void onClick(View v) {
-        mode = startActionMode(new TestActionMode());
-      }
-    });
-    ((Button) findViewById(R.id.hide)).setOnClickListener(new View.OnClickListener() {
-      @Override
-      public void onClick(View v) {
-        if (mode != null) {
-          mode.finish();
-        }
-      }
-    });
-  }
-
-
-  @Override
-  public boolean onCreateOptionsMenu(Menu menu) {
-    populate(menu);
-    return true;
-  }
-
-  @Override
-  public boolean onMenuItemSelected(int featureId, MenuItem menu) {
-    setResult(menu.getTitle());
-    return true;
-  }
-
-  private void setResult(CharSequence result) {
-    TextView text = (TextView) findViewById(R.id.textActionBarResult);
-    text.setText(result);
-  }
-
-  private void populate(Menu menu) {
-    menu.add("Save")
-        .setIcon(R.drawable.ic_action_save).setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
-    menu.add("Search")
-        .setIcon(R.drawable.ic_action_search).setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
-    menu.add("World")
-        .setIcon(R.drawable.ic_action_world).setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
-    menu.add("Key")
-        .setIcon(R.drawable.ic_action_key).setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
-    menu.add("Calendar")
-        .setIcon(R.drawable.ic_action_calendar).setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
-    menu.add("Lock")
-        .setIcon(R.drawable.ic_action_lock).setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
-  }
-          
-
-  private final class TestActionMode implements ActionMode.Callback {
-    @Override
-    public boolean onCreateActionMode(ActionMode mode, Menu menu) {
-      populate(menu);
-      return true;
-    }
-
-    @Override
-    public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
-      return false;
-    }
-
-    @Override
-    public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
-      setResult(item.getTitle());
-      return true;
-    }
-
-    @Override
-    public void onDestroyActionMode(ActionMode mode) {}
-  }
-}
diff --git a/samples/testapp/src/com/google/android/apps/common/testing/ui/testapp/DisplayActivity.java b/samples/testapp/src/com/google/android/apps/common/testing/ui/testapp/DisplayActivity.java
deleted file mode 100644
index 0340631..0000000
--- a/samples/testapp/src/com/google/android/apps/common/testing/ui/testapp/DisplayActivity.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.google.android.apps.common.testing.ui.testapp;
-
-
-import android.app.Activity;
-import android.os.Bundle;
-import android.widget.TextView;
-
-/**
- * Simple activity used to display data received from another activity.
- */
-public class DisplayActivity extends Activity {
-
-  @Override
-  public void onCreate(Bundle savedInstanceState) {
-    super.onCreate(savedInstanceState);
-    setContentView(R.layout.display_activity);
-    TextView textView = (TextView) findViewById(R.id.displayData);
-    textView.setText(getIntent().getStringExtra(SendActivity.EXTRA_DATA));
-  }
-}
diff --git a/samples/testapp/src/com/google/android/apps/common/testing/ui/testapp/GestureActivity.java b/samples/testapp/src/com/google/android/apps/common/testing/ui/testapp/GestureActivity.java
deleted file mode 100644
index 489444d..0000000
--- a/samples/testapp/src/com/google/android/apps/common/testing/ui/testapp/GestureActivity.java
+++ /dev/null
@@ -1,196 +0,0 @@
-package com.google.android.apps.common.testing.ui.testapp;
-
-import com.google.common.collect.Lists;
-
-import android.app.Activity;
-import android.os.Bundle;
-import android.util.Log;
-import android.view.GestureDetector;
-import android.view.MotionEvent;
-import android.view.View;
-
-import java.util.List;
-
-/**
- * Displays a large touchable area and logs the events it receives.
- */
-public class GestureActivity extends Activity {
-  private static final String TAG = GestureActivity.class.getSimpleName();
-
-
-  private View gestureArea;
-  private List<MotionEvent> downEvents = Lists.newArrayList();
-  private List<MotionEvent> scrollEvents = Lists.newArrayList();
-  private List<MotionEvent> longPressEvents = Lists.newArrayList();
-  private List<MotionEvent> showPresses = Lists.newArrayList();
-  private List<MotionEvent> singleTaps = Lists.newArrayList();
-  private List<MotionEvent> confirmedSingleTaps = Lists.newArrayList();
-  private List<MotionEvent> doubleTapEvents = Lists.newArrayList();
-  private List<MotionEvent> doubleTaps = Lists.newArrayList();
-
-  public void clearDownEvents() {
-    downEvents.clear();
-  }
-
-  public void clearScrollEvents() {
-    scrollEvents.clear();
-  }
-
-  public void clearLongPressEvents() {
-    longPressEvents.clear();
-  }
-
-  public void clearShowPresses() {
-    showPresses.clear();
-  }
-
-  public void clearSingleTaps() {
-    singleTaps.clear();
-  }
-
-  public void clearConfirmedSingleTaps() {
-    confirmedSingleTaps.clear();
-  }
-
-  public void clearDoubleTapEvents() {
-    doubleTapEvents.clear();
-  }
-
-  public void clearDoubleTaps() {
-    doubleTaps.clear();
-  }
-
-  public List<MotionEvent> getDownEvents() {
-    return Lists.newArrayList(downEvents);
-  }
-
-  public List<MotionEvent> getScrollEvents() {
-    return Lists.newArrayList(scrollEvents);
-  }
-
-  public List<MotionEvent> getLongPressEvents() {
-    return Lists.newArrayList(longPressEvents);
-  }
-
-  public List<MotionEvent> getShowPresses() {
-    return Lists.newArrayList(showPresses);
-  }
-
-  public List<MotionEvent> getSingleTaps() {
-    return Lists.newArrayList(singleTaps);
-  }
-
-  public List<MotionEvent> getConfirmedSingleTaps() {
-    return Lists.newArrayList(confirmedSingleTaps);
-  }
-
-  public List<MotionEvent> getDoubleTapEvents() {
-    return Lists.newArrayList(doubleTapEvents);
-  }
-
-  public List<MotionEvent> getDoubleTaps() {
-    return Lists.newArrayList(doubleTaps);
-  }
-
-  @Override
-  public void onCreate(Bundle icicle) {
-    super.onCreate(icicle);
-    setContentView(R.layout.gesture_activity);
-    gestureArea = findViewById(R.id.gestureArea);
-    final GestureDetector simpleDetector = new GestureDetector(this, new GestureListener());
-    simpleDetector.setIsLongpressEnabled(true);
-    simpleDetector.setOnDoubleTapListener(new DoubleTapListener());
-    gestureArea.setOnTouchListener(new View.OnTouchListener() {
-      @Override
-      public boolean onTouch(View v, MotionEvent m) {
-        return simpleDetector.onTouchEvent(m);
-      }
-    });
-  }
-
-
-  public void areaClicked(View v) {
-    Log.v(TAG, "onClick called!");
-  }
-
-  private class DoubleTapListener implements GestureDetector.OnDoubleTapListener {
-    @Override
-    public boolean onDoubleTap(MotionEvent e) {
-      doubleTaps.add(MotionEvent.obtain(e));
-      Log.v(TAG, "onDoubleTap: " + e);
-      setVisible(R.id.textDoubleClick);
-      return false;
-    }
-
-    @Override
-    public boolean onDoubleTapEvent(MotionEvent e) {
-      doubleTapEvents.add(MotionEvent.obtain(e));
-      Log.v(TAG, "onDoubleTapEvent: " + e);
-      return false;
-    }
-
-    @Override
-    public boolean onSingleTapConfirmed(MotionEvent e) {
-      confirmedSingleTaps.add(MotionEvent.obtain(e));
-      Log.v(TAG, "onSingleTapConfirmed: " + e);
-      return false;
-    }
-  }
-
-  private class GestureListener implements GestureDetector.OnGestureListener {
-    @Override
-    public boolean onDown(MotionEvent e) {
-      downEvents.add(MotionEvent.obtain(e));
-      Log.v(TAG, "Down: " + e);
-      return false;
-    }
-
-    @Override
-    public boolean onSingleTapUp(MotionEvent e) {
-      singleTaps.add(MotionEvent.obtain(e));
-      Log.v(TAG, "on single tap: " + e);
-      setVisible(R.id.textClick);
-      return false;
-    }
-
-    @Override
-    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distX, float distY) {
-      scrollEvents.add(MotionEvent.obtain(e1));
-      scrollEvents.add(MotionEvent.obtain(e2));
-      Log.v(TAG, "Scroll: e1: " + e1 + " e2: " + e2 + " distX: " + distX + " distY: " + distY);
-      setVisible(R.id.textSwipe);
-      return false;
-    }
-
-    @Override
-    public void onShowPress(MotionEvent e) {
-      showPresses.add(MotionEvent.obtain(e));
-      Log.v(TAG, "ShowPress: " + e);
-    }
-
-    @Override
-    public void onLongPress(MotionEvent e) {
-      longPressEvents.add(MotionEvent.obtain(e));
-      Log.v(TAG, "LongPress: " + e);
-      setVisible(R.id.textLongClick);
-    }
-
-    @Override
-    public boolean onFling(MotionEvent e1, MotionEvent e2, float veloX, float veloY) {
-      Log.v(TAG, "Fling: e1: " + e1 + " e2: " + e2 + " veloX: " + veloX + " veloY: " + veloY);
-      return false;
-    }
-  }
-
-  private void setVisible(int id) {
-    hideAll();
-    findViewById(id).setVisibility(View.VISIBLE);
-  }
-
-  private void hideAll() {
-    findViewById(R.id.textClick).setVisibility(View.GONE);
-    findViewById(R.id.textLongClick).setVisibility(View.GONE);
-    findViewById(R.id.textSwipe).setVisibility(View.GONE);
-    findViewById(R.id.textDoubleClick).setVisibility(View.GONE);
-  }
-}
diff --git a/samples/testapp/src/com/google/android/apps/common/testing/ui/testapp/LongListActivity.java b/samples/testapp/src/com/google/android/apps/common/testing/ui/testapp/LongListActivity.java
deleted file mode 100644
index 27ea551..0000000
--- a/samples/testapp/src/com/google/android/apps/common/testing/ui/testapp/LongListActivity.java
+++ /dev/null
@@ -1,58 +0,0 @@
-package com.google.android.apps.common.testing.ui.testapp;
-
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
-
-import android.app.Activity;
-import android.os.Bundle;
-import android.view.View;
-import android.widget.AdapterView;
-import android.widget.ListView;
-import android.widget.SimpleAdapter;
-import android.widget.TextView;
-
-import java.util.List;
-import java.util.Map;
-
-/**
- * An activity displaying a long list.
- */
-public class LongListActivity extends Activity {
-  public static final String STR = "STR";
-  public static final String LEN = "LEN";
-  private List<Map<String, Object>> data = Lists.newArrayList();
-
-  @Override
-  public void onCreate(Bundle bundle) {
-    super.onCreate(bundle);
-    populateData();
-    setContentView(R.layout.list_activity);
-    ((TextView) findViewById(R.id.selection_pos)).setText("");
-    ((TextView) findViewById(R.id.selection_class)).setText("");
-
-    ListView listView = (ListView) findViewById(R.id.list);
-    String[] from = new String[] {STR, LEN};
-    int[] to = new int[] {R.id.item_content, R.id.item_size};
-
-    listView.setAdapter(new SimpleAdapter(this, data, R.layout.list_item, from, to));
-    listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
-      @Override
-      public void onItemClick(
-          AdapterView<?> unusedParent, View clickedView, int position, long id) {
-        ((TextView) findViewById(R.id.selection_pos)).setText(String.valueOf(position));
-        ((TextView) findViewById(R.id.selection_class)).setText(
-            clickedView.getClass().getSimpleName());
-      }
-    });
-  }
-
-  private void populateData() {
-    for (int i = 0; i < 100; i++) {
-      Map<String, Object> dataRow = Maps.newHashMap();
-      dataRow.put(STR, "item: " + i);
-      dataRow.put(LEN, ((String) dataRow.get(STR)).length());
-      data.add(dataRow);
-    }
-  }
-
-}
diff --git a/samples/testapp/src/com/google/android/apps/common/testing/ui/testapp/MainActivity.java b/samples/testapp/src/com/google/android/apps/common/testing/ui/testapp/MainActivity.java
deleted file mode 100644
index 22641cb..0000000
--- a/samples/testapp/src/com/google/android/apps/common/testing/ui/testapp/MainActivity.java
+++ /dev/null
@@ -1,96 +0,0 @@
-package com.google.android.apps.common.testing.ui.testapp;
-
-import android.app.ListActivity;
-import android.content.Intent;
-import android.content.pm.ActivityInfo;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.os.Bundle;
-import android.util.Log;
-import android.view.View;
-import android.widget.ListView;
-import android.widget.SimpleAdapter;
-
-import java.text.Collator;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * Displays a list with all available activities.
- */
-public class MainActivity extends ListActivity {
-  private static final String TAG = MainActivity.class.getSimpleName();
-
-  private static final Comparator<Map<String, Object>> sDisplayNameComparator =
-      new Comparator<Map<String, Object>>() {
-        private final Collator collator = Collator.getInstance();
-
-        @Override
-        public int compare(Map<String, Object> map1, Map<String, Object> map2) {
-          return collator.compare(map1.get("title"), map2.get("title"));
-        }
-      };
-
-  @Override
-  public void onCreate(Bundle savedInstanceState) {
-    super.onCreate(savedInstanceState);
-
-    setListAdapter(new SimpleAdapter(
-        this, getData(), android.R.layout.simple_list_item_1, new String[] {"title"},
-        new int[] {android.R.id.text1}));
-    getListView().setTextFilterEnabled(true);
-  }
-
-  private List<Map<String, Object>> getData() {
-    List<Map<String, Object>> data = new ArrayList<Map<String, Object>>();
-
-    PackageInfo info = null;
-    try {
-      info = getPackageManager().getPackageInfo(getPackageName(), PackageManager.GET_ACTIVITIES);
-    } catch (NameNotFoundException e) {
-      Log.e(TAG, "Packageinfo not found in: " + getPackageName());
-    }
-
-    if (null == info) {
-      return data;
-    } else {
-      for (ActivityInfo activityInfo : info.activities) {
-
-        if (!activityInfo.name.equals(getComponentName().getClassName())) {
-          String[] label = activityInfo.name.split(getPackageName() + ".");
-          addItem(data, label[1],
-              createActivityIntent(activityInfo.applicationInfo.packageName, activityInfo.name));
-        }
-      }
-    }
-
-    Collections.sort(data, sDisplayNameComparator);
-    return data;
-  }
-
-  private Intent createActivityIntent(String pkg, String componentName) {
-    Intent result = new Intent();
-    result.setClassName(pkg, componentName);
-    return result;
-  }
-
-  private void addItem(List<Map<String, Object>> data, String name, Intent intent) {
-    Map<String, Object> temp = new HashMap<String, Object>();
-    temp.put("title", name);
-    temp.put("intent", intent);
-    data.add(temp);
-  }
-
-  @Override
-  protected void onListItemClick(ListView listView, View view, int position, long id) {
-    Map<?, ?> map = (Map<?, ?>) listView.getItemAtPosition(position);
-
-    Intent intent = (Intent) map.get("intent");
-    startActivity(intent);
-  }
-}
diff --git a/samples/testapp/src/com/google/android/apps/common/testing/ui/testapp/MenuActivity.java b/samples/testapp/src/com/google/android/apps/common/testing/ui/testapp/MenuActivity.java
deleted file mode 100644
index f725c4b..0000000
--- a/samples/testapp/src/com/google/android/apps/common/testing/ui/testapp/MenuActivity.java
+++ /dev/null
@@ -1,82 +0,0 @@
-package com.google.android.apps.common.testing.ui.testapp;
-
-import android.app.Activity;
-import android.os.Build;
-import android.os.Bundle;
-import android.view.ContextMenu;
-import android.view.ContextMenu.ContextMenuInfo;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.widget.PopupMenu;
-import android.widget.PopupMenu.OnMenuItemClickListener;
-import android.widget.TextView;
-
-/**
- * Shows MenuActivity with Options menu, Context menu and Popup menu. Click on a menu item changes
- * text of R.id.textMenuResult.
- */
-public class MenuActivity extends Activity {
-
-  @Override
-  protected void onCreate(Bundle savedInstanceState) {
-    super.onCreate(savedInstanceState);
-    setContentView(R.layout.menu_activity);
-    registerForContextMenu(findViewById(R.id.textContextMenu));
-  }
-
-  @Override
-  public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
-    super.onCreateContextMenu(menu, v, menuInfo);
-    MenuInflater inflater = getMenuInflater();
-    inflater.inflate(R.menu.contextmenu, menu);
-  }
-
-  @Override
-  public boolean onOptionsItemSelected(MenuItem item) {
-    TextView text = (TextView) findViewById(R.id.textMenuResult);
-    text.setText(item.getTitle());
-    return true;
-  }
-
-  @Override
-  public boolean onCreateOptionsMenu(Menu menu) {
-    MenuInflater inflater = getMenuInflater();
-    inflater.inflate(R.menu.optionsmenu, menu);
-    return true;
-  }
-
-  @Override
-  public boolean onContextItemSelected(MenuItem item) {
-    TextView text = (TextView) findViewById(R.id.textMenuResult);
-    text.setText(item.getTitle());
-    return true;
-  }
-
-  public void showPopup(View view) {
-    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
-      TextView text = (TextView) findViewById(R.id.textMenuResult);
-      text.setText("Not supported in API " + Build.VERSION.SDK_INT);
-    } else {
-      PopupMenu popup = new PopupMenu(this, view);
-      popup.setOnMenuItemClickListener(new PopupMenuListener());
-      popup.getMenuInflater().inflate(R.menu.popupmenu, popup.getMenu());
-      popup.show();
-    }
-  }
-
-  @Override
-  public boolean onMenuItemSelected(int featureId, MenuItem item) {
-    return super.onMenuItemSelected(featureId, item);
-  }
-
-  private class PopupMenuListener implements OnMenuItemClickListener {
-    @Override
-    public boolean onMenuItemClick(MenuItem item) {
-      TextView text = (TextView) findViewById(R.id.textMenuResult);
-      text.setText(item.getTitle());
-      return true;
-    }
-  }
-}
diff --git a/samples/testapp/src/com/google/android/apps/common/testing/ui/testapp/ScrollActivity.java b/samples/testapp/src/com/google/android/apps/common/testing/ui/testapp/ScrollActivity.java
deleted file mode 100644
index a2b746d..0000000
--- a/samples/testapp/src/com/google/android/apps/common/testing/ui/testapp/ScrollActivity.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package com.google.android.apps.common.testing.ui.testapp;
-
-import android.app.Activity;
-import android.os.Bundle;
-
-/**
- * An activity displaying various scroll views.
- */
-public class ScrollActivity extends Activity {
-
-  @Override
-  public void onCreate(Bundle savedInstanceState) {
-    super.onCreate(savedInstanceState);
-    setContentView(R.layout.scroll_activity);
-  }
-}
diff --git a/samples/testapp/src/com/google/android/apps/common/testing/ui/testapp/SendActivity.java b/samples/testapp/src/com/google/android/apps/common/testing/ui/testapp/SendActivity.java
deleted file mode 100644
index 47a9930..0000000
--- a/samples/testapp/src/com/google/android/apps/common/testing/ui/testapp/SendActivity.java
+++ /dev/null
@@ -1,170 +0,0 @@
-package com.google.android.apps.common.testing.ui.testapp;
-
-import android.app.Activity;
-import android.app.AlertDialog;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Bundle;
-import android.provider.ContactsContract.CommonDataKinds.Phone;
-import android.view.Gravity;
-import android.view.KeyEvent;
-import android.view.MenuInflater;
-import android.view.View;
-import android.view.View.OnKeyListener;
-import android.view.ViewGroup.LayoutParams;
-import android.widget.EditText;
-import android.widget.PopupMenu;
-import android.widget.PopupWindow;
-import android.widget.TextView;
-
-/**
- * Simple activity used for validating intent sending and UI behavior.
- */
-public class SendActivity extends Activity {
-
-  private static final int PICK_CONTACT_REQUEST = 1;  // The request code
-  static final String EXTRA_DATA = "com.google.android.apps.common.testing.ui.testapp.DATA";
-  static final int PICK_CONTACT = 100;
-  private PopupWindow popupWindow;
-
-  @Override
-  public void onCreate(Bundle savedInstanceState) {
-    super.onCreate(savedInstanceState);
-    setContentView(R.layout.send_activity);
-    
-    EditText editText = (EditText) findViewById(R.id.enterDataEditText);
-    editText.setOnKeyListener(new OnKeyListener() {
-
-      @Override
-      public boolean onKey(View view, int keyCode, KeyEvent event) {
-        if ((event.getAction() == KeyEvent.ACTION_DOWN) &&
-            (keyCode == KeyEvent.KEYCODE_ENTER)) {
-          EditText editText = (EditText) view;
-          TextView responseText = (TextView) findViewById(R.id.enterDataResponseText);
-          responseText.setText(editText.getText());
-          return true;
-        } else {
-          return false;
-        }
-      }
-    });
-  }
-
-  /** Called when user clicks the Send button */
-  public void sendData(View view) {
-    Intent intent = new Intent(this, DisplayActivity.class);
-    EditText editText = (EditText) findViewById(R.id.sendDataEditText);
-    intent.putExtra(EXTRA_DATA, editText.getText().toString());
-    startActivity(intent);
-  }
-
-  public void sendDataToCall(View view) {
-    Intent intentToCall = new Intent(Intent.ACTION_CALL);
-    EditText editText = (EditText) findViewById(R.id.sendDataToCallEditText);
-    String number = editText.getText().toString();
-    intentToCall.setData(Uri.parse("tel:" + number));
-    startActivity(intentToCall);
-  }
-
-  public void sendDataToBrowser(View view) {
-    EditText editText = (EditText) findViewById(R.id.sendDataToBrowserEditText);
-    String url = editText.getText().toString();
-    Intent intentToBrowser = new Intent(Intent.ACTION_VIEW);
-    intentToBrowser.setData(Uri.parse(url));
-    intentToBrowser.addCategory(Intent.CATEGORY_BROWSABLE);
-    intentToBrowser.putExtra("key1", "value1");
-    intentToBrowser.putExtra("key2", "value2");
-    startActivity(intentToBrowser);
-  }
-
-  public void sendMessage(View view) {
-    Intent sendIntent = new Intent();
-    EditText editText = (EditText) findViewById(R.id.sendDataToMessageEditText);
-    sendIntent.setAction(Intent.ACTION_SEND);
-    sendIntent.putExtra(Intent.EXTRA_TEXT, editText.getText().toString());
-    sendIntent.setType("text/plain");
-    startActivity(sendIntent);
-  }
-
-  public void clickToMarket(View view) {
-    Intent marketIntent = new Intent(Intent.ACTION_VIEW);
-    EditText editText = (EditText) findViewById(R.id.sendToMarketData);
-    marketIntent.setData(Uri.parse(
-        "market://details?id=" + editText.getText().toString()));
-    startActivity(marketIntent);
-  }
-  
-  public void clickToGesture(View view) {
-    startActivity(new Intent(this, GestureActivity.class));
-  }
-
-  public void clickToScroll(View view) {
-    startActivity(new Intent(this, ScrollActivity.class));
-  }
-
-  public void clickToList(View view) {
-    startActivity(new Intent(this, LongListActivity.class));
-  }
-
-  public boolean showDialog(View view) {
-    new AlertDialog.Builder(this)
-        .setTitle(R.string.dialog_title)
-        .setMessage(R.string.dialog_message)
-        .setNeutralButton("Fine", new DialogInterface.OnClickListener() {
-          @Override
-          public void onClick(DialogInterface dialog, int choice) {
-            dialog.dismiss();
-          }
-        })
-        .show();
-    return true;
-  }
-
-  public boolean showPopupView(View view) {
-    View content = getLayoutInflater().inflate(R.layout.popup_window, null, false);
-    popupWindow = new PopupWindow(content, LayoutParams.WRAP_CONTENT,
-        LayoutParams.WRAP_CONTENT, true);
-    content.setOnClickListener(new View.OnClickListener() {
-      @Override
-      public void onClick(View view) {
-        popupWindow.dismiss();
-      }
-    });
-
-    popupWindow.showAtLocation(view, Gravity.CENTER, 0, 0);
-
-    return true;
-  }
-
-  public boolean showPopupMenu(View view) {
-    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
-      return false;
-    }
-
-    PopupMenu popup = new PopupMenu(this, view);
-    MenuInflater inflater = popup.getMenuInflater();
-    inflater.inflate(R.menu.popup_menu, popup.getMenu());
-    popup.show();
-    return true;
-  }
-
-  public void pickContact(View view) {
-    Intent pickContactIntent = new Intent(Intent.ACTION_PICK, Uri.parse("content://contacts"));
-    pickContactIntent.setType(Phone.CONTENT_TYPE); // Show user only contacts w/ phone numbers
-    startActivityForResult(pickContactIntent, PICK_CONTACT_REQUEST);
-  }
-
-  @Override
-  protected void onActivityResult(int requestCode, int resultCode, Intent data) {
-    if (requestCode == PICK_CONTACT_REQUEST) {
-      if (resultCode == RESULT_OK) {
-        // TODO(valeraz): hook this up for real as shown in this example:
-        // http://developer.android.com/training/basics/intents/result.html
-        TextView textView = (TextView) findViewById(R.id.phoneNumber);
-        textView.setText(data.getExtras().getString("phone"));
-      }
-    }
-  }
-}
diff --git a/samples/testapp/tests/.classpath b/samples/testapp/tests/.classpath
deleted file mode 100644
index 711fb08..0000000
--- a/samples/testapp/tests/.classpath
+++ /dev/null
@@ -1,10 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<classpath>
-	<classpathentry kind="src" path="src"/>
-	<classpathentry combineaccessrules="false" kind="src" path="/testapp"/>
-	<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
-	<classpathentry combineaccessrules="false" exported="true" kind="src" path="/DroidDriver"/>
-	<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
-	<classpathentry kind="src" path="gen"/>
-	<classpathentry kind="output" path="bin/classes"/>
-</classpath>
diff --git a/samples/testapp/tests/.project b/samples/testapp/tests/.project
deleted file mode 100644
index 9e7c767..0000000
--- a/samples/testapp/tests/.project
+++ /dev/null
@@ -1,33 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<projectDescription>
-	<name>testappTest</name>
-	<comment></comment>
-	<projects>
-	</projects>
-	<buildSpec>
-		<buildCommand>
-			<name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
-			<arguments>
-			</arguments>
-		</buildCommand>
-		<buildCommand>
-			<name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
-			<arguments>
-			</arguments>
-		</buildCommand>
-		<buildCommand>
-			<name>org.eclipse.jdt.core.javabuilder</name>
-			<arguments>
-			</arguments>
-		</buildCommand>
-		<buildCommand>
-			<name>com.android.ide.eclipse.adt.ApkBuilder</name>
-			<arguments>
-			</arguments>
-		</buildCommand>
-	</buildSpec>
-	<natures>
-		<nature>com.android.ide.eclipse.adt.AndroidNature</nature>
-		<nature>org.eclipse.jdt.core.javanature</nature>
-	</natures>
-</projectDescription>
diff --git a/samples/testapp/tests/Android.mk b/samples/testapp/tests/Android.mk
deleted file mode 100644
index 32edfbe..0000000
--- a/samples/testapp/tests/Android.mk
+++ /dev/null
@@ -1,17 +0,0 @@
-LOCAL_PATH:= $(call my-dir)
-include $(CLEAR_VARS)
-
-# We only want this apk build for tests.
-LOCAL_MODULE_TAGS := tests optional
-
-LOCAL_STATIC_JAVA_LIBRARIES += droiddriver
-
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
-
-LOCAL_PACKAGE_NAME := droiddriver.samples.testapp.tests
-
-LOCAL_INSTRUMENTATION_FOR := droiddriver.samples.testapp
-
-LOCAL_SDK_VERSION := 16
-
-include $(BUILD_PACKAGE)
diff --git a/samples/testapp/tests/AndroidManifest.xml b/samples/testapp/tests/AndroidManifest.xml
deleted file mode 100644
index c9e46d1..0000000
--- a/samples/testapp/tests/AndroidManifest.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.google.android.apps.common.testing.ui.testapp.test"
-    android:versionCode="1"
-    android:versionName="1.0" >
-
-    <uses-sdk
-        android:minSdkVersion="7"
-        android:targetSdkVersion="16" />
-
-    <instrumentation
-        android:name="com.google.android.droiddriver.runner.TestRunner"
-        android:targetPackage="com.google.android.apps.common.testing.ui.testapp" />
-
-    <application>
-        <uses-library android:name="android.test.runner" />
-    </application>
-
-</manifest>
\ No newline at end of file
diff --git a/samples/testapp/tests/proguard-project.txt b/samples/testapp/tests/proguard-project.txt
deleted file mode 100644
index f2fe155..0000000
--- a/samples/testapp/tests/proguard-project.txt
+++ /dev/null
@@ -1,20 +0,0 @@
-# To enable ProGuard in your project, edit project.properties
-# to define the proguard.config property as described in that file.
-#
-# Add project specific ProGuard rules here.
-# By default, the flags in this file are appended to flags specified
-# in ${sdk.dir}/tools/proguard/proguard-android.txt
-# You can edit the include path and order by changing the ProGuard
-# include property in project.properties.
-#
-# For more details, see
-#   http://developer.android.com/guide/developing/tools/proguard.html
-
-# Add any project specific keep options here:
-
-# If your project uses WebView with JS, uncomment the following
-# and specify the fully qualified class name to the JavaScript interface
-# class:
-#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
-#   public *;
-#}
diff --git a/samples/testapp/tests/project.properties b/samples/testapp/tests/project.properties
deleted file mode 100644
index 9b84a6b..0000000
--- a/samples/testapp/tests/project.properties
+++ /dev/null
@@ -1,14 +0,0 @@
-# This file is automatically generated by Android Tools.
-# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
-#
-# This file must be checked in Version Control Systems.
-#
-# To customize properties used by the Ant build system edit
-# "ant.properties", and override values to adapt the script to your
-# project structure.
-#
-# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
-#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
-
-# Project target.
-target=android-16
diff --git a/samples/testapp/tests/res/.README.txt b/samples/testapp/tests/res/.README.txt
deleted file mode 100644
index 912f51b..0000000
--- a/samples/testapp/tests/res/.README.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-This file keeps eclipse happy as it doesn't seem able to create the res folder it doesn't actually need. It can be removed if the res folder ever gets real content.
-Starting the file with a dot keeps aapt (res compiler) happy.
diff --git a/samples/testapp/tests/src/com/google/android/apps/common/testing/ui/testapp/AbstractSendActivityTest.java b/samples/testapp/tests/src/com/google/android/apps/common/testing/ui/testapp/AbstractSendActivityTest.java
deleted file mode 100644
index 4824b4e..0000000
--- a/samples/testapp/tests/src/com/google/android/apps/common/testing/ui/testapp/AbstractSendActivityTest.java
+++ /dev/null
@@ -1,47 +0,0 @@
-package com.google.android.apps.common.testing.ui.testapp;
-
-import android.test.ActivityInstrumentationTestCase2;
-
-import com.google.android.droiddriver.finders.By;
-import com.google.android.droiddriver.finders.XPaths;
-import com.google.android.droiddriver.DroidDriver;
-
-/**
- * Base class for testing SendActivity.
- */
-// google3/javatests/com/google/android/apps/common/testing/ui/espresso/exampletest/ExampleTest.java
-public abstract class AbstractSendActivityTest extends
-    ActivityInstrumentationTestCase2<SendActivity> {
-
-  private DroidDriver driver;
-  private SendActivity activity;
-
-  public AbstractSendActivityTest() {
-    super(SendActivity.class);
-  }
-
-  protected abstract DroidDriver getDriver();
-
-  @Override
-  public void setUp() throws Exception {
-    super.setUp();
-    if (driver == null) {
-      driver = getDriver();
-    }
-    activity = getActivity();
-  }
-
-  public void testClick() {
-    driver.on(By.text(activity.getString(R.string.button_send))).click();
-    assertTrue(driver.on(By.text(getDisplayTitle())).isVisible());
-  }
-
-  public void testClickXPath() {
-    driver.on(By.xpath("//ScrollView//Button")).click();
-    assertTrue(driver.on(By.xpath("//TextView" + XPaths.text(getDisplayTitle()))).isVisible());
-  }
-
-  private String getDisplayTitle() {
-    return activity.getString(R.string.display_title);
-  }
-}
diff --git a/samples/testapp/tests/src/com/google/android/apps/common/testing/ui/testapp/sendactivity/Default.java b/samples/testapp/tests/src/com/google/android/apps/common/testing/ui/testapp/sendactivity/Default.java
deleted file mode 100644
index 23f3c12..0000000
--- a/samples/testapp/tests/src/com/google/android/apps/common/testing/ui/testapp/sendactivity/Default.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.google.android.apps.common.testing.ui.testapp.sendactivity;
-
-import com.google.android.apps.common.testing.ui.testapp.AbstractSendActivityTest;
-import com.google.android.droiddriver.DroidDriverBuilder;
-import com.google.android.droiddriver.DroidDriver;
-
-public class Default extends AbstractSendActivityTest {
-  @Override
-  protected DroidDriver getDriver() {
-    return new DroidDriverBuilder(getInstrumentation()).build();
-  }
-}
\ No newline at end of file
diff --git a/samples/testapp/tests/src/com/google/android/apps/common/testing/ui/testapp/sendactivity/UseInstrumentation.java b/samples/testapp/tests/src/com/google/android/apps/common/testing/ui/testapp/sendactivity/UseInstrumentation.java
deleted file mode 100644
index 3ec650e..0000000
--- a/samples/testapp/tests/src/com/google/android/apps/common/testing/ui/testapp/sendactivity/UseInstrumentation.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.google.android.apps.common.testing.ui.testapp.sendactivity;
-
-import com.google.android.apps.common.testing.ui.testapp.AbstractSendActivityTest;
-import com.google.android.droiddriver.DroidDriverBuilder;
-import com.google.android.droiddriver.DroidDriver;
-import com.google.android.droiddriver.DroidDriverBuilder.Implementation;
-
-public class UseInstrumentation extends AbstractSendActivityTest {
-  @Override
-  protected DroidDriver getDriver() {
-    return new DroidDriverBuilder(getInstrumentation()).use(Implementation.INSTRUMENTATION).build();
-  }
-}
\ No newline at end of file
diff --git a/samples/testapp/tests/src/com/google/android/apps/common/testing/ui/testapp/sendactivity/UseUiAutomation.java b/samples/testapp/tests/src/com/google/android/apps/common/testing/ui/testapp/sendactivity/UseUiAutomation.java
deleted file mode 100644
index 9849510..0000000
--- a/samples/testapp/tests/src/com/google/android/apps/common/testing/ui/testapp/sendactivity/UseUiAutomation.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.google.android.apps.common.testing.ui.testapp.sendactivity;
-
-import com.google.android.apps.common.testing.ui.testapp.AbstractSendActivityTest;
-import com.google.android.droiddriver.DroidDriver;
-import com.google.android.droiddriver.DroidDriverBuilder;
-import com.google.android.droiddriver.DroidDriverBuilder.Implementation;
-
-// Optional annotation to show filtering tests by build version.
-@com.google.android.droiddriver.runner.UseUiAutomation
-public class UseUiAutomation extends AbstractSendActivityTest {
-  @Override
-  protected DroidDriver getDriver() {
-    return new DroidDriverBuilder(getInstrumentation()).use(Implementation.UI_AUTOMATION).build();
-  }
-}
diff --git a/samples/testapp/tests/src/com/google/android/droiddriver/finders/XPathsTest.java b/samples/testapp/tests/src/com/google/android/droiddriver/finders/XPathsTest.java
deleted file mode 100644
index cabd483..0000000
--- a/samples/testapp/tests/src/com/google/android/droiddriver/finders/XPathsTest.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.google.android.droiddriver.finders;
-
-import junit.framework.TestCase;
-
-/**
- * Test XPaths.
- */
-public class XPathsTest extends TestCase {
-  public void testQuoteXPathLiteral() {
-    assertEquals("concat(\"a\",'\"',\"'b\")", XPaths.quoteXPathLiteral("a\"'b"));
-  }
-}
diff --git a/src/com/google/android/droiddriver/DroidDriver.java b/src/com/google/android/droiddriver/DroidDriver.java
index 6daed71..cd1caca 100644
--- a/src/com/google/android/droiddriver/DroidDriver.java
+++ b/src/com/google/android/droiddriver/DroidDriver.java
@@ -20,6 +20,9 @@
 import com.google.android.droiddriver.exceptions.TimeoutException;
 import com.google.android.droiddriver.finders.Finder;
 
+/**
+ * The entry interface for using droiddriver.
+ */
 public interface DroidDriver {
   /**
    * Returns whether a matching element exists without polling. Use this if the
@@ -65,9 +68,9 @@
 
   /**
    * Returns the first {@link UiElement} found using the given finder without
-   * polling. This method is useful in {@link Poller.PollingListener#onPolling}.
-   * In other situations polling is desired, and {@link #on} is more
-   * appropriate.
+   * polling and without {@link #refreshUiElementTree}. This method is useful in
+   * {@link Poller.PollingListener#onPolling}. In other situations polling is
+   * desired, and {@link #on} is more appropriate.
    *
    * @param finder The matching mechanism
    * @return The first matching element
@@ -76,8 +79,15 @@
   UiElement find(Finder finder);
 
   /**
+   * Refreshes the UiElement tree. All methods in this interface that take a
+   * Finder parameter call this method, unless noted otherwise.
+   */
+  void refreshUiElementTree();
+
+  /**
    * Polls until a {@link UiElement} is found using the given finder, or the
-   * default timeout is reached.
+   * default timeout is reached. This behaves the same as {@link #on} except
+   * that it does not return the {@link UiElement}.
    *
    * @param finder The matching mechanism
    * @throws TimeoutException If matching element does not appear within the
@@ -106,10 +116,18 @@
   void setPoller(Poller poller);
 
   /**
+   * Returns a {@link UiDevice} for device-wide interaction.
+   */
+  UiDevice getUiDevice();
+
+  /**
    * Dumps the UiElement tree to a file to help debug. The tree is based on the
-   * last used root UiElement if it exists. Screenshot is always current. If
-   * they do not match, the UiElement tree must be stale, indicating that you
-   * should use a fresh UiElement instead of an old instance.
+   * last used root UiElement if it exists, otherwise
+   * {@link #refreshUiElementTree} is called.
+   * <p>
+   * The dump may contain invisible UiElements that are not used in the finding
+   * algorithm.
+   * </p>
    *
    * @param path the path of file to save the tree
    * @return whether the dumping succeeded
diff --git a/src/com/google/android/droiddriver/DroidDriverBuilder.java b/src/com/google/android/droiddriver/DroidDriverBuilder.java
deleted file mode 100644
index 87ecafc..0000000
--- a/src/com/google/android/droiddriver/DroidDriverBuilder.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * 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;
-
-import android.app.Instrumentation;
-import android.os.Build;
-
-import com.google.android.droiddriver.exceptions.DroidDriverException;
-import com.google.android.droiddriver.instrumentation.InstrumentationDriver;
-import com.google.android.droiddriver.uiautomation.UiAutomationDriver;
-import com.google.common.base.Preconditions;
-
-/**
- * Builds DroidDriver instances.
- */
-public class DroidDriverBuilder {
-  public enum Implementation {
-    ANY, INSTRUMENTATION, UI_AUTOMATION
-  }
-
-  private final Instrumentation instrumentation;
-  private Implementation implementation;
-
-  public DroidDriverBuilder(Instrumentation instrumentation) {
-    this.instrumentation = Preconditions.checkNotNull(instrumentation);
-  }
-
-  public DroidDriver build() {
-    if (implementation == null) {
-      implementation = Implementation.ANY;
-    }
-    switch (implementation) {
-      case INSTRUMENTATION:
-        return new InstrumentationDriver(instrumentation);
-      case UI_AUTOMATION:
-        return getUiAutomationDriver();
-      case ANY:
-        if (hasUiAutomation()) {
-          return getUiAutomationDriver();
-        }
-        return new InstrumentationDriver(instrumentation);
-    }
-    // should never reach here
-    throw new DroidDriverException("Cannot build DroidDriver");
-  }
-
-  private boolean hasUiAutomation() {
-    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
-      return true;
-    }
-    // TODO: remove after mr2 is released?
-    // This is necessary because now mr2 build has ro.build.version.sdk=17
-    try {
-      instrumentation.getUiAutomation();
-      return true;
-    } catch (NoSuchMethodError e) {
-      return false;
-    }
-  }
-
-  private UiAutomationDriver getUiAutomationDriver() {
-    if (!hasUiAutomation()) {
-      throw new DroidDriverException("UI_AUTOMATION is not available below API 18");
-    }
-    if (instrumentation.getUiAutomation() == null) {
-        throw new DroidDriverException(
-                "uiAutomation==null: did you forget to set '-w' flag for 'am instrument'?");
-    }
-    return new UiAutomationDriver(instrumentation);
-  }
-
-  public DroidDriverBuilder use(Implementation implementation) {
-    Preconditions.checkState(this.implementation == null,
-        "Cannot set implementation more than once");
-    this.implementation = Preconditions.checkNotNull(implementation);
-    return this;
-  }
-}
diff --git a/src/com/google/android/droiddriver/Poller.java b/src/com/google/android/droiddriver/Poller.java
index 24cce77..548f1ad 100644
--- a/src/com/google/android/droiddriver/Poller.java
+++ b/src/com/google/android/droiddriver/Poller.java
@@ -106,10 +106,13 @@
   ConditionChecker<Void> GONE = new ConditionChecker<Void>() {
     @Override
     public Void check(DroidDriver driver, Finder finder) throws UnsatisfiedConditionException {
-      if (driver.has(finder)) {
+      try {
+        // "find" does not call refreshUiElementTree, while "has" calls
+        driver.find(finder);
         throw new UnsatisfiedConditionException();
+      } catch (ElementNotFoundException enfe) {
+        return null;
       }
-      return null;
     }
 
     @Override
diff --git a/src/com/google/android/droiddriver/Screenshotter.java b/src/com/google/android/droiddriver/Screenshotter.java
deleted file mode 100644
index 473509d..0000000
--- a/src/com/google/android/droiddriver/Screenshotter.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * 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;
-
-import android.graphics.Bitmap.CompressFormat;
-
-/**
- * Interface for taking screenshot.
- *
- * @see UiAutomationDriver
- */
-public interface Screenshotter {
-  /**
-   * Takes a screenshot of current window and stores it in {@code path} as PNG.
-   *
-   * @param path the path of file to save screenshot
-   * @return true if screen shot is created successfully
-   */
-  boolean takeScreenshot(String path);
-
-  /**
-   * Takes a screenshot of current window and stores it in {@code path}.
-   *
-   * @param path the path of file to save screenshot
-   * @param format The format of the compressed image
-   * @param quality Hint to the compressor, 0-100. 0 meaning compress for small
-   *        size, 100 meaning compress for max quality. Some formats, like PNG
-   *        which is lossless, will ignore the quality setting
-   * @return true if screen shot is created successfully
-   */
-  boolean takeScreenshot(String path, CompressFormat format, int quality);
-}
diff --git a/src/com/google/android/droiddriver/UiDevice.java b/src/com/google/android/droiddriver/UiDevice.java
new file mode 100644
index 0000000..84fea1b
--- /dev/null
+++ b/src/com/google/android/droiddriver/UiDevice.java
@@ -0,0 +1,75 @@
+/*
+ * 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;
+
+import android.graphics.Bitmap.CompressFormat;
+
+import com.google.android.droiddriver.actions.Action;
+
+/**
+ * Interface for device-wide interaction.
+ */
+public interface UiDevice {
+  /**
+   * Returns whether the screen is on.
+   */
+  boolean isScreenOn();
+
+  /** Wakes up device if the screen is off */
+  void wakeUp();
+
+  /** Puts device to sleep if the screen is on */
+  void sleep();
+
+  /** Simulates pressing "back" button */
+  void pressBack();
+
+  /**
+   * Executes a global action without the context of a certain UiElement.
+   *
+   * @param action The action to execute
+   * @return true if the action is successful
+   */
+  boolean perform(Action action);
+
+  /**
+   * Takes a screenshot of current window and stores it in {@code path} as PNG.
+   * <p>
+   * If this is used in a test which extends
+   * {@link android.test.ActivityInstrumentationTestCase2}, call this before
+   * {@code tearDown()} because {@code tearDown()} finishes activities created
+   * by {@link android.test.ActivityInstrumentationTestCase2#getActivity()}.
+   *
+   * @param path the path of file to save screenshot
+   * @return true if screen shot is created successfully
+   */
+  boolean takeScreenshot(String path);
+
+  /**
+   * Takes a screenshot of current window and stores it in {@code path}. Note
+   * some implementations may not capture everything on the screen, for example
+   * InstrumentationDriver may not see the IME soft keyboard or system content.
+   *
+   * @param path the path of file to save screenshot
+   * @param format The format of the compressed image
+   * @param quality Hint to the compressor, 0-100. 0 meaning compress for small
+   *        size, 100 meaning compress for max quality. Some formats, like PNG
+   *        which is lossless, will ignore the quality setting
+   * @return true if screen shot is created successfully
+   */
+  boolean takeScreenshot(String path, CompressFormat format, int quality);
+}
diff --git a/src/com/google/android/droiddriver/UiElement.java b/src/com/google/android/droiddriver/UiElement.java
index 6541da0..71b43ed 100644
--- a/src/com/google/android/droiddriver/UiElement.java
+++ b/src/com/google/android/droiddriver/UiElement.java
@@ -19,12 +19,12 @@
 import android.graphics.Rect;
 
 import com.google.android.droiddriver.actions.Action;
-import com.google.android.droiddriver.actions.ScrollDirection;
-import com.google.android.droiddriver.exceptions.ElementNotVisibleException;
+import com.google.android.droiddriver.actions.InputInjector;
 import com.google.android.droiddriver.finders.Attribute;
+import com.google.android.droiddriver.finders.Predicate;
 import com.google.android.droiddriver.instrumentation.InstrumentationDriver;
+import com.google.android.droiddriver.scroll.Direction.PhysicalDirection;
 import com.google.android.droiddriver.uiautomation.UiAutomationDriver;
-import com.google.common.base.Predicate;
 
 import java.util.List;
 
@@ -140,7 +140,6 @@
    * Executes the given action.
    *
    * @param action The action to execute
-   * @throws ElementNotVisibleException when the element is not visible
    * @return true if the action is successful
    */
   boolean perform(Action action);
@@ -149,45 +148,57 @@
    * Sets the text of this element.
    *
    * @param text The text to enter.
-   * @throws ElementNotVisibleException when the element is not visible
    */
-  // TODO: Should this clear the text before setting?
   void setText(String text);
 
   /**
    * Clicks this element. The click will be at the center of the visible
    * element.
-   *
-   * @throws ElementNotVisibleException when the element is not visible
    */
   void click();
 
   /**
    * Long-clicks this element. The click will be at the center of the visible
    * element.
-   *
-   * @throws ElementNotVisibleException when the element is not visible
    */
   void longClick();
 
   /**
    * Double-clicks this element. The click will be at the center of the visible
    * element.
-   *
-   * @throws ElementNotVisibleException when the element is not visible
    */
   void doubleClick();
 
   /**
-   * Scrolls in the given direction. Scrolling down means swiping upwards.
+   * Scrolls in the given direction.
+   *
+   * @param direction specifies where the view port will move, instead of the
+   *        finger.
    */
-  void scroll(ScrollDirection direction);
+  void scroll(PhysicalDirection direction);
 
   /**
    * Gets an immutable {@link List} of immediate children that satisfy
-   * {@code predicate}. It always filters children that are null.
+   * {@code predicate}. It always filters children that are null. This gives a
+   * low level access to the underlying data. Do not use it unless you are sure
+   * about the subtle details. Note the count may not be what you expect. For
+   * instance, a dynamic list may show more items when scrolling beyond the end,
+   * varying the count. The count also depends on the driver implementation:
+   * <ul>
+   * <li>{@link InstrumentationDriver} includes all.</li>
+   * <li>the Accessibility API (which {@link UiAutomationDriver} depends on)
+   * does not include off-screen children, but may include invisible on-screen
+   * children.</li>
+   * </ul>
+   * <p>
+   * Another discrepancy between {@link InstrumentationDriver}
+   * {@link UiAutomationDriver} is the order of children. The Accessibility API
+   * returns children in the order of layout (see
+   * {@link android.view.ViewGroup#addChildrenForAccessibility}, which is added
+   * in API16).
+   * </p>
    */
-  List<UiElement> getChildren(Predicate<? super UiElement> predicate);
+  List<? extends UiElement> getChildren(Predicate<? super UiElement> predicate);
 
   /**
    * Filters out invisible children.
@@ -204,29 +215,13 @@
     }
   };
 
-  // TODO: remove getChildCount and getChild.
-  /**
-   * Gets the child at given index.
-   */
-  UiElement getChild(int index);
-
-  /**
-   * Gets the child count. This gives a low level access to the underlying data.
-   * Do not use it unless you are sure about the subtle details. Note the count
-   * may not be what you expect. For instance, a dynamic list may show more
-   * items when scrolling beyond the end, varying the count. The count also
-   * depends on the driver implementation:
-   * <ul>
-   * <li>{@link InstrumentationDriver} includes all.</li>
-   * <li>the Accessibility API (which {@link UiAutomationDriver} depends on)
-   * does not include off-screen children, but may include invisible on-screen
-   * children.</li>
-   * </ul>
-   */
-  int getChildCount();
-
   /**
    * Gets the parent.
    */
   UiElement getParent();
+
+  /**
+   * Gets the {@link InputInjector} for injecting InputEvent.
+   */
+  InputInjector getInjector();
 }
diff --git a/src/com/google/android/droiddriver/actions/Action.java b/src/com/google/android/droiddriver/actions/Action.java
index 1c57e12..fd5068e 100644
--- a/src/com/google/android/droiddriver/actions/Action.java
+++ b/src/com/google/android/droiddriver/actions/Action.java
@@ -16,26 +16,23 @@
 
 package com.google.android.droiddriver.actions;
 
-import android.view.InputEvent;
-
-import com.google.android.droiddriver.InputInjector;
 import com.google.android.droiddriver.UiElement;
 
 /**
  * Interface for performing action on a UiElement. An action is a high-level
- * user interaction that consists of a series of {@link InputEvent}s.
+ * user interaction that can be performed in various ways, for example, via
+ * synthesized events, or the Accessibility API.
  */
 public interface Action {
   /**
    * Performs the action.
    *
-   * @param injector the injector to inject {@link InputEvent}s
    * @param element the Ui element to perform the action on
    * @return Whether the action is successful. Some actions throw exceptions in
    *         case of failure, when that behavior is more appropriate. For
-   *         example, ClickAction.
+   *         example, if event injection returns false.
    */
-  boolean perform(InputInjector injector, UiElement element);
+  boolean perform(UiElement element);
 
   /**
    * Gets the timeout to wait for an indicator that the action has been carried
@@ -51,7 +48,7 @@
    *
    * <p>
    * It is recommended that this method return the description of the action,
-   * for example, "TypeAction{text to type}".
+   * for example, "SwipeAction{DOWN}".
    */
   @Override
   String toString();
diff --git a/src/com/google/android/droiddriver/actions/ClickAction.java b/src/com/google/android/droiddriver/actions/ClickAction.java
index 12db023..0d7350d 100644
--- a/src/com/google/android/droiddriver/actions/ClickAction.java
+++ b/src/com/google/android/droiddriver/actions/ClickAction.java
@@ -20,14 +20,13 @@
 import android.os.SystemClock;
 import android.view.ViewConfiguration;
 
-import com.google.android.droiddriver.InputInjector;
 import com.google.android.droiddriver.UiElement;
 import com.google.android.droiddriver.util.Events;
 
 /**
  * An action that does clicks on an UiElement.
  */
-public abstract class ClickAction extends BaseAction {
+public abstract class ClickAction extends EventAction {
 
   public static final ClickAction SINGLE = new SingleClick(1000L);
   public static final ClickAction LONG = new LongClick(1000L);
@@ -42,13 +41,13 @@
 
     @Override
     public boolean perform(InputInjector injector, UiElement element) {
-      SINGLE.perform(injector, element);
-      SINGLE.perform(injector, element);
+      SINGLE.perform(element);
+      SINGLE.perform(element);
       return true;
     }
   }
 
-  private static class LongClick extends ClickAction {
+  public static class LongClick extends ClickAction {
     public LongClick(long timeoutMillis) {
       super(timeoutMillis);
     }
diff --git a/src/com/google/android/droiddriver/actions/EventAction.java b/src/com/google/android/droiddriver/actions/EventAction.java
new file mode 100644
index 0000000..f2736e3
--- /dev/null
+++ b/src/com/google/android/droiddriver/actions/EventAction.java
@@ -0,0 +1,46 @@
+/*
+ * 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;
+
+import android.view.InputEvent;
+
+import com.google.android.droiddriver.UiElement;
+
+/**
+ * Implements {@link Action} by injecting synthesized events.
+ */
+public abstract class EventAction extends BaseAction {
+  protected EventAction(long timeoutMillis) {
+    super(timeoutMillis);
+  }
+
+  @Override
+  public boolean perform(UiElement element) {
+    return perform(element.getInjector(), element);
+  }
+
+  /**
+   * Performs the action by injecting synthesized events.
+   *
+   * @param injector the injector to inject {@link InputEvent}s
+   * @param element the UiElement to perform the action on
+   * @return Whether the action is successful. Some actions throw exceptions in
+   *         case of failure, when that behavior is more appropriate. For
+   *         example, if event injection returns false.
+   */
+  protected abstract boolean perform(InputInjector injector, UiElement element);
+}
diff --git a/src/com/google/android/droiddriver/actions/EventUiElementActor.java b/src/com/google/android/droiddriver/actions/EventUiElementActor.java
new file mode 100644
index 0000000..1913e5d
--- /dev/null
+++ b/src/com/google/android/droiddriver/actions/EventUiElementActor.java
@@ -0,0 +1,53 @@
+/*
+ * 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;
+
+import com.google.android.droiddriver.UiElement;
+import com.google.android.droiddriver.scroll.Direction.PhysicalDirection;
+
+/**
+ * A {@link UiElementActor} that performs actions by injecting synthesized
+ * events.
+ */
+public class EventUiElementActor implements UiElementActor {
+  public static final EventUiElementActor INSTANCE = new EventUiElementActor();
+
+  @Override
+  public void setText(UiElement uiElement, String text) {
+    uiElement.perform(new TextAction(text));
+  }
+
+  @Override
+  public void click(UiElement uiElement) {
+    uiElement.perform(ClickAction.SINGLE);
+  }
+
+  @Override
+  public void longClick(UiElement uiElement) {
+    uiElement.perform(ClickAction.LONG);
+  }
+
+  @Override
+  public void doubleClick(UiElement uiElement) {
+    uiElement.perform(ClickAction.DOUBLE);
+  }
+
+  @Override
+  public void scroll(UiElement uiElement, PhysicalDirection direction) {
+    uiElement.perform(SwipeAction.toScroll(direction));
+  }
+}
diff --git a/src/com/google/android/droiddriver/InputInjector.java b/src/com/google/android/droiddriver/actions/InputInjector.java
similarity index 94%
rename from src/com/google/android/droiddriver/InputInjector.java
rename to src/com/google/android/droiddriver/actions/InputInjector.java
index 987bb7d..96dc07b 100644
--- a/src/com/google/android/droiddriver/InputInjector.java
+++ b/src/com/google/android/droiddriver/actions/InputInjector.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.google.android.droiddriver;
+package com.google.android.droiddriver.actions;
 
 import android.view.InputEvent;
 
diff --git a/src/com/google/android/droiddriver/actions/KeyAction.java b/src/com/google/android/droiddriver/actions/KeyAction.java
index 1cafe0e..18bbd4c 100644
--- a/src/com/google/android/droiddriver/actions/KeyAction.java
+++ b/src/com/google/android/droiddriver/actions/KeyAction.java
@@ -16,11 +16,23 @@
 
 package com.google.android.droiddriver.actions;
 
+import com.google.android.droiddriver.UiElement;
+import com.google.android.droiddriver.exceptions.ActionException;
+
 /**
  * Base class for {@link Action} that injects key events.
  */
-public abstract class KeyAction extends BaseAction {
-  protected KeyAction(long timeoutMillis) {
+public abstract class KeyAction extends EventAction {
+  private final boolean checkFocused;
+
+  protected KeyAction(long timeoutMillis, boolean checkFocused) {
     super(timeoutMillis);
+    this.checkFocused = checkFocused;
+  }
+
+  protected void maybeCheckFocused(UiElement element) {
+    if (checkFocused && element != null && !element.isFocused()) {
+      throw new ActionException(element + " is not focused");
+    }
   }
 }
diff --git a/src/com/google/android/droiddriver/actions/PressKeyAction.java b/src/com/google/android/droiddriver/actions/PressKeyAction.java
deleted file mode 100644
index 14fd060..0000000
--- a/src/com/google/android/droiddriver/actions/PressKeyAction.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * 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;
-
-import android.os.SystemClock;
-import android.view.KeyEvent;
-
-import com.google.android.droiddriver.InputInjector;
-import com.google.android.droiddriver.UiElement;
-import com.google.android.droiddriver.util.Events;
-import com.google.common.base.Objects;
-
-/**
- * An action to press a single key. TODO: rename to SingleKeyAction
- */
-public class PressKeyAction extends KeyAction {
-  private final int keyCode;
-
-  /**
-   * Defaults timeoutMillis to 0.
-   */
-  public PressKeyAction(int keyCode) {
-    this(keyCode, 0L);
-  }
-
-  public PressKeyAction(int keyCode, long timeoutMillis) {
-    super(timeoutMillis);
-    this.keyCode = keyCode;
-  }
-
-  @Override
-  public boolean perform(InputInjector injector, UiElement element) {
-    final long downTime = SystemClock.uptimeMillis();
-    KeyEvent downEvent = Events.newKeyEvent(downTime, KeyEvent.ACTION_DOWN, keyCode);
-    KeyEvent upEvent = Events.newKeyEvent(downTime, KeyEvent.ACTION_UP, keyCode);
-
-    return injector.injectInputEvent(downEvent) && injector.injectInputEvent(upEvent);
-  }
-
-  @Override
-  public String toString() {
-    return Objects.toStringHelper(this).addValue(KeyEvent.keyCodeToString(keyCode)).toString();
-  }
-}
diff --git a/src/com/google/android/droiddriver/actions/ScrollAction.java b/src/com/google/android/droiddriver/actions/ScrollAction.java
index e616e09..6305c83 100644
--- a/src/com/google/android/droiddriver/actions/ScrollAction.java
+++ b/src/com/google/android/droiddriver/actions/ScrollAction.java
@@ -17,10 +17,7 @@
 package com.google.android.droiddriver.actions;
 
 /**
- * Base class for {@link Action} that scrolls.
+ * Marker interface for a scroll action.
  */
-public abstract class ScrollAction extends BaseAction {
-  protected ScrollAction(long timeoutMillis) {
-    super(timeoutMillis);
-  }
+public interface ScrollAction {
 }
diff --git a/src/com/google/android/droiddriver/actions/ScrollDirection.java b/src/com/google/android/droiddriver/actions/ScrollDirection.java
deleted file mode 100644
index 422ef95..0000000
--- a/src/com/google/android/droiddriver/actions/ScrollDirection.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * 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;
-
-/**
- * Scroll directions.
- */
-public enum ScrollDirection {
-  UP,
-  DOWN,
-  LEFT,
-  RIGHT
-}
diff --git a/src/com/google/android/droiddriver/actions/SingleKeyAction.java b/src/com/google/android/droiddriver/actions/SingleKeyAction.java
new file mode 100644
index 0000000..853840e
--- /dev/null
+++ b/src/com/google/android/droiddriver/actions/SingleKeyAction.java
@@ -0,0 +1,75 @@
+/*
+ * 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;
+
+import android.os.Build;
+import android.os.SystemClock;
+import android.view.KeyEvent;
+
+import com.google.android.droiddriver.UiElement;
+import com.google.android.droiddriver.util.Events;
+import com.google.android.droiddriver.util.Strings;
+
+/**
+ * An action to press a single key. While it is convenient for navigating the
+ * UI, do not overuse it -- the application may interpret key codes in a custom
+ * way and, more importantly, application users may not have access to it
+ * because the device (physical or virtual keyboard) may not support all key
+ * codes.
+ */
+public class SingleKeyAction extends KeyAction {
+  /**
+   * Common instances for convenience and memory preservation.
+   */
+  public static final SingleKeyAction MENU = new SingleKeyAction(KeyEvent.KEYCODE_MENU);
+  public static final SingleKeyAction SEARCH = new SingleKeyAction(KeyEvent.KEYCODE_SEARCH);
+  public static final SingleKeyAction BACK = new SingleKeyAction(KeyEvent.KEYCODE_BACK);
+  public static final SingleKeyAction DELETE = new SingleKeyAction(KeyEvent.KEYCODE_DEL);
+
+  private final int keyCode;
+
+  /**
+   * Defaults timeoutMillis to 100.
+   */
+  public SingleKeyAction(int keyCode) {
+    this(keyCode, 100L, false);
+  }
+
+  public SingleKeyAction(int keyCode, long timeoutMillis, boolean checkFocused) {
+    super(timeoutMillis, checkFocused);
+    this.keyCode = keyCode;
+  }
+
+  @Override
+  public boolean perform(InputInjector injector, UiElement element) {
+    maybeCheckFocused(element);
+
+    final long downTime = SystemClock.uptimeMillis();
+    KeyEvent downEvent = Events.newKeyEvent(downTime, KeyEvent.ACTION_DOWN, keyCode);
+    KeyEvent upEvent = Events.newKeyEvent(downTime, KeyEvent.ACTION_UP, keyCode);
+
+    return injector.injectInputEvent(downEvent) && injector.injectInputEvent(upEvent);
+  }
+
+  @Override
+  public String toString() {
+    String keyCodeString =
+        Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB_MR1 ? String.valueOf(keyCode)
+            : KeyEvent.keyCodeToString(keyCode);
+    return Strings.toStringHelper(this).addValue(keyCodeString).toString();
+  }
+}
diff --git a/src/com/google/android/droiddriver/actions/SwipeAction.java b/src/com/google/android/droiddriver/actions/SwipeAction.java
index 433f6de..6837741 100644
--- a/src/com/google/android/droiddriver/actions/SwipeAction.java
+++ b/src/com/google/android/droiddriver/actions/SwipeAction.java
@@ -20,39 +20,138 @@
 import android.os.SystemClock;
 import android.view.ViewConfiguration;
 
-import com.google.android.droiddriver.InputInjector;
 import com.google.android.droiddriver.UiElement;
 import com.google.android.droiddriver.exceptions.ActionException;
+import com.google.android.droiddriver.scroll.Direction.PhysicalDirection;
 import com.google.android.droiddriver.util.Events;
+import com.google.android.droiddriver.util.Strings;
+import com.google.android.droiddriver.util.Strings.ToStringHelper;
 
 /**
- * A {@link ScrollAction} that swipes the touch screen.
+ * An action that swipes the touch screen.
  */
-public class SwipeAction extends ScrollAction {
-
-  private final ScrollDirection direction;
-  private final boolean drag;
-
+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.
+  private static final int ACTION_MOVE_INTERVAL = 5;
   /**
-   * Defaults timeoutMillis to 0.
+   * The magic number from UiAutomator. This value is empirical. If it actually
+   * results in a fling, you can change it with {@link #setScrollSteps}.
    */
-  public SwipeAction(ScrollDirection direction, boolean drag) {
-    this(direction, drag, 0L);
+  private static int scrollSteps = 55;
+  private static int flingSteps = 3;
+
+  /** Returns the {@link #scrollSteps} used in {@link #toScroll}. */
+  public static int getScrollSteps() {
+    return scrollSteps;
   }
 
-  public SwipeAction(ScrollDirection direction, boolean drag, long timeoutMillis) {
+  /** Sets the {@link #scrollSteps} used in {@link #toScroll}. */
+  public static void setScrollSteps(int scrollSteps) {
+    SwipeAction.scrollSteps = scrollSteps;
+  }
+
+  /** Returns the {@link #flingSteps} used in {@link #toFling}. */
+  public static int getFlingSteps() {
+    return flingSteps;
+  }
+
+  /** Sets the {@link #flingSteps} used in {@link #toFling}. */
+  public static void setFlingSteps(int flingSteps) {
+    SwipeAction.flingSteps = flingSteps;
+  }
+
+  /**
+   * Gets {@link SwipeAction} instances for scrolling.
+   * <p>
+   * Note: This may result in flinging instead of scrolling, depending on the
+   * size of the target UiElement and the SDK version of the device. If it does
+   * not behave as expected, you can change steps with {@link #setScrollSteps}.
+   * </p>
+   *
+   * @param direction specifies where the view port will move, instead of the
+   *        finger.
+   * @see ViewConfiguration#getScaledMinimumFlingVelocity
+   */
+  public static SwipeAction toScroll(PhysicalDirection direction) {
+    return new SwipeAction(direction, scrollSteps);
+  }
+
+  /**
+   * Gets {@link SwipeAction} instances for flinging.
+   * <p>
+   * Note: This may not actually fling, depending on the size of the target
+   * UiElement and the SDK version of the device. If it does not behave as
+   * expected, you can change steps with {@link #setFlingSteps}.
+   * </p>
+   *
+   * @param direction specifies where the view port will move, instead of the
+   *        finger.
+   * @see ViewConfiguration#getScaledMinimumFlingVelocity
+   */
+  public static SwipeAction toFling(PhysicalDirection direction) {
+    return new SwipeAction(direction, flingSteps);
+  }
+
+  private final PhysicalDirection direction;
+  private final boolean drag;
+  private final int steps;
+  private final float topMarginRatio;
+  private final float leftMarginRatio;
+  private final float bottomMarginRatio;
+  private final float rightMarginRatio;
+
+  /**
+   * Defaults timeoutMillis to 1000 and no drag.
+   */
+  public SwipeAction(PhysicalDirection direction, int steps) {
+    this(direction, steps, false, 1000L);
+  }
+
+  /**
+   * Defaults all margin ratios to 0.1F.
+   */
+  public SwipeAction(PhysicalDirection direction, int steps, boolean drag, long timeoutMillis) {
+    this(direction, steps, drag, timeoutMillis, 0.1F, 0.1F, 0.1F, 0.1F);
+  }
+
+  /**
+   * @param direction the scroll direction specifying where the view port will
+   *        move, instead of the finger.
+   * @param steps minimum 2; (steps-1) is the number of {@code ACTION_MOVE} that
+   *        will be injected between {@code ACTION_DOWN} and {@code ACTION_UP}.
+   * @param drag whether this is a drag
+   * @param timeoutMillis
+   * @param topMarginRatio margin ratio from top
+   * @param leftMarginRatio margin ratio from left
+   * @param bottomMarginRatio margin ratio from bottom
+   * @param rightMarginRatio margin ratio from right
+   */
+  public SwipeAction(PhysicalDirection direction, int steps, boolean drag, long timeoutMillis,
+      float topMarginRatio, float leftMarginRatio, float bottomMarginRatio, float rightMarginRatio) {
     super(timeoutMillis);
     this.direction = direction;
+    this.steps = Math.max(2, steps);
     this.drag = drag;
+    this.topMarginRatio = topMarginRatio;
+    this.bottomMarginRatio = bottomMarginRatio;
+    this.leftMarginRatio = leftMarginRatio;
+    this.rightMarginRatio = rightMarginRatio;
   }
 
   @Override
   public boolean perform(InputInjector injector, UiElement element) {
     Rect elementRect = element.getVisibleBounds();
 
-    int swipeAreaHeightAdjust = (int) (elementRect.height() * 0.1);
-    int swipeAreaWidthAdjust = (int) (elementRect.width() * 0.1);
-    int steps = 50;
+    int topMargin = (int) (elementRect.height() * topMarginRatio);
+    int bottomMargin = (int) (elementRect.height() * bottomMarginRatio);
+    int leftMargin = (int) (elementRect.width() * leftMarginRatio);
+    int rightMargin = (int) (elementRect.width() * rightMarginRatio);
+    int adjustedbottom = elementRect.bottom - bottomMargin;
+    int adjustedTop = elementRect.top + topMargin;
+    int adjustedLeft = elementRect.left + leftMargin;
+    int adjustedRight = elementRect.right - rightMargin;
     int startX;
     int startY;
     int endX;
@@ -61,26 +160,26 @@
     switch (direction) {
       case DOWN:
         startX = elementRect.centerX();
-        startY = elementRect.bottom - swipeAreaHeightAdjust;
+        startY = adjustedbottom;
         endX = elementRect.centerX();
-        endY = elementRect.top + swipeAreaHeightAdjust;
+        endY = adjustedTop;
         break;
       case UP:
         startX = elementRect.centerX();
-        startY = elementRect.top + swipeAreaHeightAdjust;
+        startY = adjustedTop;
         endX = elementRect.centerX();
-        endY = elementRect.bottom - swipeAreaHeightAdjust;
+        endY = adjustedbottom;
         break;
       case LEFT:
-        startX = elementRect.left + swipeAreaWidthAdjust;
+        startX = adjustedLeft;
         startY = elementRect.centerY();
-        endX = elementRect.right - swipeAreaWidthAdjust;
+        endX = adjustedRight;
         endY = elementRect.centerY();
         break;
       case RIGHT:
-        startX = elementRect.right - swipeAreaWidthAdjust;
+        startX = adjustedRight;
         startY = elementRect.centerY();
-        endX = elementRect.left + swipeAreaHeightAdjust;
+        endX = adjustedLeft;
         endY = elementRect.centerY();
         break;
       default:
@@ -92,12 +191,13 @@
 
     // First touch starts exactly at the point requested
     long downTime = Events.touchDown(injector, startX, startY);
+    SystemClock.sleep(ACTION_MOVE_INTERVAL);
     if (drag) {
       SystemClock.sleep((long) (ViewConfiguration.getLongPressTimeout() * 1.5f));
     }
     for (int i = 1; i < steps; i++) {
       Events.touchMove(injector, downTime, startX + (int) (xStep * i), startY + (int) (yStep * i));
-      SystemClock.sleep(5);
+      SystemClock.sleep(ACTION_MOVE_INTERVAL);
     }
     if (drag) {
       // Hold final position for a little bit to simulate drag.
@@ -106,4 +206,15 @@
     Events.touchUp(injector, downTime, endX, endY);
     return true;
   }
+
+  @Override
+  public String toString() {
+    ToStringHelper toStringHelper = Strings.toStringHelper(this);
+    toStringHelper.addValue(direction);
+    toStringHelper.add("steps", steps);
+    if (drag) {
+      toStringHelper.addValue("drag");
+    }
+    return toStringHelper.toString();
+  }
 }
diff --git a/src/com/google/android/droiddriver/actions/TypeAction.java b/src/com/google/android/droiddriver/actions/TextAction.java
similarity index 72%
rename from src/com/google/android/droiddriver/actions/TypeAction.java
rename to src/com/google/android/droiddriver/actions/TextAction.java
index 55e8dea..586ad4e 100644
--- a/src/com/google/android/droiddriver/actions/TypeAction.java
+++ b/src/com/google/android/droiddriver/actions/TextAction.java
@@ -16,40 +16,45 @@
 
 package com.google.android.droiddriver.actions;
 
-import com.google.android.droiddriver.InputInjector;
-import com.google.android.droiddriver.UiElement;
-import com.google.android.droiddriver.exceptions.ActionException;
-import com.google.common.base.Objects;
-import com.google.common.base.Preconditions;
-
+import android.os.Build;
 import android.os.SystemClock;
 import android.view.KeyCharacterMap;
 import android.view.KeyEvent;
 
+import com.google.android.droiddriver.UiElement;
+import com.google.android.droiddriver.exceptions.ActionException;
+import com.google.android.droiddriver.util.Preconditions;
+import com.google.android.droiddriver.util.Strings;
+
 /**
  * An action to type text.
  */
-public class TypeAction extends KeyAction {
+public class TextAction extends KeyAction {
 
-  private static final KeyCharacterMap KEY_CHAR_MAP = KeyCharacterMap
-      .load(KeyCharacterMap.VIRTUAL_KEYBOARD);
+  @SuppressWarnings("deprecation")
+  private static final KeyCharacterMap KEY_CHAR_MAP =
+      Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB ? KeyCharacterMap
+          .load(KeyCharacterMap.BUILT_IN_KEYBOARD) : KeyCharacterMap
+          .load(KeyCharacterMap.VIRTUAL_KEYBOARD);
 
   private final String text;
 
   /**
-   * Defaults timeoutMillis to 0.
+   * Defaults timeoutMillis to 100.
    */
-  public TypeAction(String text) {
-    this(text, 0L);
+  public TextAction(String text) {
+    this(text, 100L, false);
   }
 
-  public TypeAction(String text, long timeoutMillis) {
-    super(timeoutMillis);
+  public TextAction(String text, long timeoutMillis, boolean checkFocused) {
+    super(timeoutMillis, checkFocused);
     this.text = Preconditions.checkNotNull(text);
   }
 
   @Override
   public boolean perform(InputInjector injector, UiElement element) {
+    maybeCheckFocused(element);
+
     // TODO: recycle events?
     KeyEvent[] events = KEY_CHAR_MAP.getEvents(text.toCharArray());
     boolean success = false;
@@ -75,6 +80,6 @@
 
   @Override
   public String toString() {
-    return Objects.toStringHelper(this).addValue(text).toString();
+    return Strings.toStringHelper(this).addValue(text).toString();
   }
 }
diff --git a/src/com/google/android/droiddriver/actions/UiElementActor.java b/src/com/google/android/droiddriver/actions/UiElementActor.java
new file mode 100644
index 0000000..abca286
--- /dev/null
+++ b/src/com/google/android/droiddriver/actions/UiElementActor.java
@@ -0,0 +1,58 @@
+/*
+ * 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;
+
+import com.google.android.droiddriver.UiElement;
+import com.google.android.droiddriver.scroll.Direction.PhysicalDirection;
+
+/**
+ * Interface for performing actions on a {@link UiElement}.
+ */
+public interface UiElementActor {
+  /**
+   * Sets the text of this element.
+   *
+   * @param text The text to enter.
+   */
+  void setText(UiElement uiElement, String text);
+
+  /**
+   * Clicks this element. The click will be at the center of the visible
+   * element.
+   */
+  void click(UiElement uiElement);
+
+  /**
+   * Long-clicks this element. The click will be at the center of the visible
+   * element.
+   */
+  void longClick(UiElement uiElement);
+
+  /**
+   * Double-clicks this element. The click will be at the center of the visible
+   * element.
+   */
+  void doubleClick(UiElement uiElement);
+
+  /**
+   * Scrolls in the given direction.
+   *
+   * @param direction specifies where the view port will move, instead of the
+   *        finger.
+   */
+  void scroll(UiElement uiElement, PhysicalDirection direction);
+}
diff --git a/src/com/google/android/droiddriver/actions/accessibility/AccessibilityAction.java b/src/com/google/android/droiddriver/actions/accessibility/AccessibilityAction.java
new file mode 100644
index 0000000..5edc131
--- /dev/null
+++ b/src/com/google/android/droiddriver/actions/accessibility/AccessibilityAction.java
@@ -0,0 +1,48 @@
+/*
+ * 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.accessibility;
+
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import com.google.android.droiddriver.UiElement;
+import com.google.android.droiddriver.actions.Action;
+import com.google.android.droiddriver.actions.BaseAction;
+import com.google.android.droiddriver.uiautomation.UiAutomationElement;
+
+/**
+ * Implements {@link Action} via the Accessibility API.
+ */
+public abstract class AccessibilityAction extends BaseAction {
+  protected AccessibilityAction(long timeoutMillis) {
+    super(timeoutMillis);
+  }
+
+  @Override
+  public boolean perform(UiElement element) {
+    return perform(((UiAutomationElement) element).getRawElement(), element);
+  }
+
+  /**
+   * Performs the action via the Accessibility API.
+   *
+   * @param node the AccessibilityNodeInfo used to create the UiElement
+   * @param element the UiElement to perform the action on
+   * @return Whether the action is successful. Some actions throw exceptions in
+   *         case of failure, when that behavior is more appropriate.
+   */
+  protected abstract boolean perform(AccessibilityNodeInfo node, UiElement element);
+}
diff --git a/src/com/google/android/droiddriver/actions/accessibility/AccessibilityClickAction.java b/src/com/google/android/droiddriver/actions/accessibility/AccessibilityClickAction.java
new file mode 100644
index 0000000..0e7cc2b
--- /dev/null
+++ b/src/com/google/android/droiddriver/actions/accessibility/AccessibilityClickAction.java
@@ -0,0 +1,82 @@
+/*
+ * 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.accessibility;
+
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import com.google.android.droiddriver.UiElement;
+import com.google.android.droiddriver.exceptions.ActionException;
+
+/**
+ * An {@link AccessibilityAction} that clicks on a UiElement.
+ */
+public abstract class AccessibilityClickAction extends AccessibilityAction {
+
+  public static final AccessibilityClickAction SINGLE = new SingleClick(1000L);
+  public static final AccessibilityClickAction LONG = new LongClick(1000L);
+  public static final AccessibilityClickAction DOUBLE = new DoubleClick(1000L);
+
+  protected AccessibilityClickAction(long timeoutMillis) {
+    super(timeoutMillis);
+  }
+
+  public static class DoubleClick extends AccessibilityClickAction {
+    public DoubleClick(long timeoutMillis) {
+      super(timeoutMillis);
+    }
+
+    @Override
+    protected boolean perform(AccessibilityNodeInfo node, UiElement element) {
+      return SINGLE.perform(element) && SINGLE.perform(element);
+    }
+  }
+
+  public static class LongClick extends AccessibilityClickAction {
+    public LongClick(long timeoutMillis) {
+      super(timeoutMillis);
+    }
+
+    @Override
+    protected boolean perform(AccessibilityNodeInfo node, UiElement element) {
+      if (!element.isLongClickable()) {
+        throw new ActionException(element
+            + " is not long-clickable; maybe there is a clickable element in the same location?");
+      }
+      return node.performAction(AccessibilityNodeInfo.ACTION_LONG_CLICK);
+    }
+  }
+
+  public static class SingleClick extends AccessibilityClickAction {
+    public SingleClick(long timeoutMillis) {
+      super(timeoutMillis);
+    }
+
+    @Override
+    protected boolean perform(AccessibilityNodeInfo node, UiElement element) {
+      if (!element.isClickable()) {
+        throw new ActionException(element
+            + " is not clickable; maybe there is a clickable element in the same location?");
+      }
+      return node.performAction(AccessibilityNodeInfo.ACTION_CLICK);
+    }
+  }
+
+  @Override
+  public String toString() {
+    return getClass().getSimpleName();
+  }
+}
diff --git a/src/com/google/android/droiddriver/actions/accessibility/AccessibilityScrollAction.java b/src/com/google/android/droiddriver/actions/accessibility/AccessibilityScrollAction.java
new file mode 100644
index 0000000..bdeddc8
--- /dev/null
+++ b/src/com/google/android/droiddriver/actions/accessibility/AccessibilityScrollAction.java
@@ -0,0 +1,62 @@
+/*
+ * 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.accessibility;
+
+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 implements ScrollAction {
+  private final PhysicalDirection direction;
+
+  public AccessibilityScrollAction(PhysicalDirection direction) {
+    this(direction, 1000L);
+  }
+
+  public AccessibilityScrollAction(PhysicalDirection direction, long timeoutMillis) {
+    super(timeoutMillis);
+    this.direction = direction;
+  }
+
+  @Override
+  protected boolean perform(AccessibilityNodeInfo node, UiElement element) {
+    if (!element.isScrollable()) {
+      return false;
+    }
+
+    switch (direction) {
+      case UP:
+      case LEFT:
+        return node.performAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
+      case DOWN:
+      case RIGHT:
+        return node.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
+    }
+    return false;
+  }
+
+  @Override
+  public String toString() {
+    return Strings.toStringHelper(this).addValue(direction).toString();
+  }
+}
diff --git a/src/com/google/android/droiddriver/actions/accessibility/AccessibilityUiElementActor.java b/src/com/google/android/droiddriver/actions/accessibility/AccessibilityUiElementActor.java
new file mode 100644
index 0000000..dec5979
--- /dev/null
+++ b/src/com/google/android/droiddriver/actions/accessibility/AccessibilityUiElementActor.java
@@ -0,0 +1,54 @@
+/*
+ * 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.accessibility;
+
+import com.google.android.droiddriver.UiElement;
+import com.google.android.droiddriver.actions.TextAction;
+import com.google.android.droiddriver.actions.UiElementActor;
+import com.google.android.droiddriver.scroll.Direction.PhysicalDirection;
+
+/**
+ * A {@link UiElementActor} that performs actions via the Accessibility API.
+ */
+public class AccessibilityUiElementActor implements UiElementActor {
+  public static final AccessibilityUiElementActor INSTANCE = new AccessibilityUiElementActor();
+
+  @Override
+  public void setText(UiElement uiElement, String text) {
+    uiElement.perform(new TextAction(text));
+  }
+
+  @Override
+  public void click(UiElement uiElement) {
+    uiElement.perform(AccessibilityClickAction.SINGLE);
+  }
+
+  @Override
+  public void longClick(UiElement uiElement) {
+    uiElement.perform(AccessibilityClickAction.LONG);
+  }
+
+  @Override
+  public void doubleClick(UiElement uiElement) {
+    uiElement.perform(AccessibilityClickAction.DOUBLE);
+  }
+
+  @Override
+  public void scroll(UiElement uiElement, PhysicalDirection direction) {
+    uiElement.perform(new AccessibilityScrollAction(direction));
+  }
+}
diff --git a/src/com/google/android/droiddriver/base/AbstractContext.java b/src/com/google/android/droiddriver/base/AbstractContext.java
deleted file mode 100644
index 2c831ee..0000000
--- a/src/com/google/android/droiddriver/base/AbstractContext.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * 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.base;
-
-import com.google.android.droiddriver.InputInjector;
-
-/**
- * Internal helper for managing all instances.
- */
-public abstract class AbstractContext {
-  protected final InputInjector injector;
-
-  protected AbstractContext(InputInjector injector) {
-    this.injector = injector;
-  }
-
-  public InputInjector getInjector() {
-    return injector;
-  }
-
-  /** Clears data in the context */
-  public abstract void clearData();
-}
diff --git a/src/com/google/android/droiddriver/base/AbstractDroidDriver.java b/src/com/google/android/droiddriver/base/AbstractDroidDriver.java
deleted file mode 100644
index ed78b46..0000000
--- a/src/com/google/android/droiddriver/base/AbstractDroidDriver.java
+++ /dev/null
@@ -1,163 +0,0 @@
-/*
- * 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.base;
-
-import android.app.Instrumentation;
-import android.graphics.Bitmap;
-import android.graphics.Bitmap.CompressFormat;
-import android.util.Log;
-
-import com.google.android.droiddriver.DroidDriver;
-import com.google.android.droiddriver.Poller;
-import com.google.android.droiddriver.Screenshotter;
-import com.google.android.droiddriver.UiElement;
-import com.google.android.droiddriver.exceptions.ElementNotFoundException;
-import com.google.android.droiddriver.exceptions.TimeoutException;
-import com.google.android.droiddriver.finders.ByXPath;
-import com.google.android.droiddriver.finders.Finder;
-import com.google.android.droiddriver.util.DefaultPoller;
-import com.google.android.droiddriver.util.FileUtils;
-import com.google.android.droiddriver.util.Logs;
-import com.google.common.base.Preconditions;
-
-import java.io.BufferedOutputStream;
-
-/**
- * Abstract implementation of DroidDriver that does the common actions, and
- * should not differ in implementations of {@link DroidDriver}.
- */
-public abstract class AbstractDroidDriver implements DroidDriver, Screenshotter {
-
-  protected final Instrumentation instrumentation;
-  private Poller poller = new DefaultPoller();
-  private AbstractUiElement rootElement;
-
-  protected AbstractDroidDriver(Instrumentation instrumentation) {
-    this.instrumentation = Preconditions.checkNotNull(instrumentation);
-  }
-
-  @Override
-  public UiElement find(Finder finder) {
-    Logs.call(this, "find", finder);
-    return finder.find(refreshRootElement());
-  }
-
-  @Override
-  public boolean has(Finder finder) {
-    try {
-      find(finder);
-      return true;
-    } catch (ElementNotFoundException enfe) {
-      return false;
-    }
-  }
-
-  @Override
-  public boolean has(Finder finder, long timeoutMillis) {
-    try {
-      getPoller().pollFor(this, finder, Poller.EXISTS, timeoutMillis);
-      return true;
-    } catch (TimeoutException e) {
-      return false;
-    }
-  }
-
-  @Override
-  public UiElement on(Finder finder) {
-    Logs.call(this, "on", finder);
-    return getPoller().pollFor(this, finder, Poller.EXISTS);
-  }
-
-  @Override
-  public void checkExists(Finder finder) {
-    Logs.call(this, "checkExists", finder);
-    getPoller().pollFor(this, finder, Poller.EXISTS);
-  }
-
-  @Override
-  public void checkGone(Finder finder) {
-    Logs.call(this, "checkGone", finder);
-    getPoller().pollFor(this, finder, Poller.GONE);
-  }
-
-  @Override
-  public Poller getPoller() {
-    return poller;
-  }
-
-  @Override
-  public void setPoller(Poller poller) {
-    this.poller = poller;
-  }
-
-  protected abstract AbstractUiElement getNewRootElement();
-
-  protected abstract AbstractContext getContext();
-
-  protected AbstractUiElement getRootElement() {
-    if (rootElement == null) {
-      refreshRootElement();
-    }
-    return rootElement;
-  }
-
-  private AbstractUiElement refreshRootElement() {
-    getContext().clearData();
-    rootElement = getNewRootElement();
-    return rootElement;
-  }
-
-  @Override
-  public boolean dumpUiElementTree(String path) {
-    Logs.call(this, "dumpUiElementTree", path);
-    return ByXPath.dumpDom(path, getRootElement());
-  }
-
-  @Override
-  public boolean takeScreenshot(String path) {
-    return takeScreenshot(path, Bitmap.CompressFormat.PNG, 0);
-  }
-
-  @Override
-  public boolean takeScreenshot(String path, CompressFormat format, int quality) {
-    Logs.call(this, "takeScreenshot", path, quality);
-    Bitmap screenshot = takeScreenshot();
-    if (screenshot == null) {
-      return false;
-    }
-    BufferedOutputStream bos = null;
-    try {
-      bos = FileUtils.open(path);
-      screenshot.compress(format, quality, bos);
-      return true;
-    } catch (Exception e) {
-      Logs.log(Log.WARN, e);
-      return false;
-    } finally {
-      if (bos != null) {
-        try {
-          bos.close();
-        } catch (Exception e) {
-          // ignore
-        }
-      }
-      screenshot.recycle();
-    }
-  }
-
-  protected abstract Bitmap takeScreenshot();
-}
diff --git a/src/com/google/android/droiddriver/base/AbstractUiElement.java b/src/com/google/android/droiddriver/base/AbstractUiElement.java
deleted file mode 100644
index 65048f2..0000000
--- a/src/com/google/android/droiddriver/base/AbstractUiElement.java
+++ /dev/null
@@ -1,186 +0,0 @@
-/*
- * 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.base;
-
-import com.google.android.droiddriver.InputInjector;
-import com.google.android.droiddriver.UiElement;
-import com.google.android.droiddriver.actions.Action;
-import com.google.android.droiddriver.actions.ClickAction;
-import com.google.android.droiddriver.actions.ScrollDirection;
-import com.google.android.droiddriver.actions.SwipeAction;
-import com.google.android.droiddriver.actions.TypeAction;
-import com.google.android.droiddriver.exceptions.ElementNotVisibleException;
-import com.google.android.droiddriver.finders.Attribute;
-import com.google.android.droiddriver.finders.ByXPath;
-import com.google.android.droiddriver.util.Logs;
-import com.google.common.base.Objects;
-import com.google.common.base.Objects.ToStringHelper;
-import com.google.common.base.Predicate;
-import com.google.common.base.Predicates;
-import com.google.common.collect.ImmutableList;
-
-import org.w3c.dom.Element;
-
-import java.lang.ref.WeakReference;
-import java.util.List;
-import java.util.concurrent.Callable;
-import java.util.concurrent.FutureTask;
-
-/**
- * Abstract implementation with common methods already implemented.
- */
-public abstract class AbstractUiElement implements UiElement {
-  private WeakReference<Element> domNode;
-
-  @Override
-  public <T> T get(Attribute attribute) {
-    return attribute.getValue(this);
-  }
-
-  @Override
-  public boolean perform(Action action) {
-    Logs.call(this, "perform", action);
-    checkVisible();
-    return performAndWait(action);
-  }
-
-  protected boolean doPerform(Action action) {
-    return action.perform(getInjector(), this);
-  }
-
-  protected void doPerformAndWait(FutureTask<Boolean> futureTask, long timeoutMillis) {
-    // ignores timeoutMillis; subclasses can override this behavior
-    futureTask.run();
-  }
-
-  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() {
-        return doPerform(action);
-      }
-    });
-    doPerformAndWait(futureTask, action.getTimeoutMillis());
-    try {
-      return futureTask.get();
-    } catch (Exception e) {
-      // should not reach here b/c futureTask has run
-      return false;
-    }
-  }
-
-  @Override
-  public void setText(String text) {
-    // TODO: Define common actions as a const.
-    perform(new TypeAction(text));
-    // TypeAction may not be effective immediately and reflected bygetText(),
-    // so the following will fail.
-    // if (Logs.DEBUG) {
-    // String actual = getText();
-    // if (!text.equals(actual)) {
-    // throw new DroidDriverException(String.format(
-    // "setText failed: expected=\"%s\", actual=\"%s\"", text, actual));
-    // }
-    // }
-  }
-
-  @Override
-  public void click() {
-    perform(ClickAction.SINGLE);
-  }
-
-  @Override
-  public void longClick() {
-    perform(ClickAction.LONG);
-  }
-
-  @Override
-  public void doubleClick() {
-    perform(ClickAction.DOUBLE);
-  }
-
-  @Override
-  public void scroll(ScrollDirection direction) {
-    perform(new SwipeAction(direction, false));
-  }
-
-  @Override
-  public abstract AbstractUiElement getChild(int index);
-
-  protected abstract InputInjector getInjector();
-
-  private void checkVisible() {
-    if (!isVisible()) {
-      throw new ElementNotVisibleException(this);
-    }
-  }
-
-  @Override
-  public List<UiElement> getChildren(Predicate<? super UiElement> predicate) {
-    predicate = Predicates.and(Predicates.notNull(), predicate);
-    // TODO: Use internal data when we take snapshot of current node tree.
-    ImmutableList.Builder<UiElement> builder = ImmutableList.builder();
-    for (int i = 0; i < getChildCount(); i++) {
-      UiElement child = getChild(i);
-      if (predicate.apply(child)) {
-        builder.add(child);
-      }
-    }
-    return builder.build();
-  }
-
-  @Override
-  public String toString() {
-    ToStringHelper toStringHelper = Objects.toStringHelper(this);
-    for (Attribute attr : Attribute.values()) {
-      addAttribute(toStringHelper, attr, get(attr));
-    }
-    return toStringHelper.toString();
-  }
-
-  private static void addAttribute(ToStringHelper toStringHelper, Attribute attr, Object value) {
-    if (value != null) {
-      if (value instanceof Boolean) {
-        if ((Boolean) value) {
-          toStringHelper.addValue(attr.getName());
-        }
-      } else {
-        toStringHelper.add(attr.getName(), value);
-      }
-    }
-  }
-
-  /**
-   * Used internally in {@link ByXPath}. Returns the DOM node representing this
-   * UiElement. The DOM is constructed from the UiElement tree.
-   * <p>
-   * TODO: move this to {@link ByXPath}. This requires a BiMap using
-   * WeakReference for both keys and values, which is error-prone. This will be
-   * deferred until we decide whether to clear cache upon getRootElement.
-   */
-  public Element getDomNode() {
-    if (domNode == null || domNode.get() == null) {
-      domNode = new WeakReference<Element>(ByXPath.buildDomNode(this));
-    }
-    return domNode.get();
-  }
-}
diff --git a/src/com/google/android/droiddriver/base/BaseDroidDriver.java b/src/com/google/android/droiddriver/base/BaseDroidDriver.java
new file mode 100644
index 0000000..b881b91
--- /dev/null
+++ b/src/com/google/android/droiddriver/base/BaseDroidDriver.java
@@ -0,0 +1,118 @@
+/*
+ * 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.base;
+
+import com.google.android.droiddriver.DroidDriver;
+import com.google.android.droiddriver.Poller;
+import com.google.android.droiddriver.UiElement;
+import com.google.android.droiddriver.actions.InputInjector;
+import com.google.android.droiddriver.exceptions.ElementNotFoundException;
+import com.google.android.droiddriver.exceptions.TimeoutException;
+import com.google.android.droiddriver.finders.ByXPath;
+import com.google.android.droiddriver.finders.Finder;
+import com.google.android.droiddriver.util.Logs;
+
+/**
+ * Base DroidDriver that implements the common operations.
+ */
+public abstract class BaseDroidDriver<R, E extends BaseUiElement<R, E>> implements DroidDriver {
+
+  private Poller poller = new DefaultPoller();
+  private E rootElement;
+
+  @Override
+  public UiElement find(Finder finder) {
+    Logs.call(this, "find", finder);
+    return finder.find(getRootElement());
+  }
+
+  @Override
+  public boolean has(Finder finder) {
+    try {
+      refreshUiElementTree();
+      find(finder);
+      return true;
+    } catch (ElementNotFoundException enfe) {
+      return false;
+    }
+  }
+
+  @Override
+  public boolean has(Finder finder, long timeoutMillis) {
+    try {
+      getPoller().pollFor(this, finder, Poller.EXISTS, timeoutMillis);
+      return true;
+    } catch (TimeoutException e) {
+      return false;
+    }
+  }
+
+  @Override
+  public UiElement on(Finder finder) {
+    Logs.call(this, "on", finder);
+    return getPoller().pollFor(this, finder, Poller.EXISTS);
+  }
+
+  @Override
+  public void checkExists(Finder finder) {
+    Logs.call(this, "checkExists", finder);
+    getPoller().pollFor(this, finder, Poller.EXISTS);
+  }
+
+  @Override
+  public void checkGone(Finder finder) {
+    Logs.call(this, "checkGone", finder);
+    getPoller().pollFor(this, finder, Poller.GONE);
+  }
+
+  @Override
+  public Poller getPoller() {
+    return poller;
+  }
+
+  @Override
+  public void setPoller(Poller poller) {
+    this.poller = poller;
+  }
+
+  public abstract InputInjector getInjector();
+
+  protected abstract E newRootElement();
+
+  /**
+   * Returns a new UiElement of type {@code E}.
+   */
+  protected abstract E newUiElement(R rawElement, E parent);
+
+  public E getRootElement() {
+    if (rootElement == null) {
+      refreshUiElementTree();
+    }
+    return rootElement;
+  }
+
+  @Override
+  public void refreshUiElementTree() {
+    rootElement = newRootElement();
+  }
+
+  @Override
+  public boolean dumpUiElementTree(String path) {
+    Logs.call(this, "dumpUiElementTree", path);
+    return ByXPath.dumpDom(path, getRootElement());
+  }
+}
diff --git a/src/com/google/android/droiddriver/base/BaseUiDevice.java b/src/com/google/android/droiddriver/base/BaseUiDevice.java
new file mode 100644
index 0000000..e9c6426
--- /dev/null
+++ b/src/com/google/android/droiddriver/base/BaseUiDevice.java
@@ -0,0 +1,113 @@
+/*
+ * 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.base;
+
+import android.app.Service;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.CompressFormat;
+import android.os.PowerManager;
+import android.util.Log;
+import android.view.KeyEvent;
+
+import com.google.android.droiddriver.UiDevice;
+import com.google.android.droiddriver.actions.Action;
+import com.google.android.droiddriver.actions.SingleKeyAction;
+import com.google.android.droiddriver.util.FileUtils;
+import com.google.android.droiddriver.util.Logs;
+
+import java.io.BufferedOutputStream;
+
+/**
+ * Base implementation of {@link UiDevice}.
+ */
+public abstract class BaseUiDevice implements UiDevice {
+  // power off may not trigger new events
+  private static final SingleKeyAction POWER_OFF = new SingleKeyAction(KeyEvent.KEYCODE_POWER, 0,
+      false);
+  // power on should always trigger new events
+  private static final SingleKeyAction POWER_ON = new SingleKeyAction(KeyEvent.KEYCODE_POWER,
+      1000L, false);
+
+  @SuppressWarnings("deprecation")
+  @Override
+  public boolean isScreenOn() {
+    PowerManager pm =
+        (PowerManager) getContext().getInstrumentation().getTargetContext()
+            .getSystemService(Service.POWER_SERVICE);
+    return pm.isScreenOn();
+  }
+
+  @Override
+  public void wakeUp() {
+    if (!isScreenOn()) {
+      perform(POWER_ON);
+    }
+  }
+
+  @Override
+  public void sleep() {
+    if (isScreenOn()) {
+      perform(POWER_OFF);
+    }
+  }
+
+  @Override
+  public void pressBack() {
+    perform(SingleKeyAction.BACK);
+  }
+
+  @Override
+  public boolean perform(Action action) {
+    return getContext().getDriver().getRootElement().perform(action);
+  }
+
+  @Override
+  public boolean takeScreenshot(String path) {
+    return takeScreenshot(path, Bitmap.CompressFormat.PNG, 0);
+  }
+
+  @Override
+  public boolean takeScreenshot(String path, CompressFormat format, int quality) {
+    Logs.call(this, "takeScreenshot", path, quality);
+    Bitmap screenshot = takeScreenshot();
+    if (screenshot == null) {
+      return false;
+    }
+    BufferedOutputStream bos = null;
+    try {
+      bos = FileUtils.open(path);
+      screenshot.compress(format, quality, bos);
+      return true;
+    } catch (Exception e) {
+      Logs.log(Log.WARN, e);
+      return false;
+    } finally {
+      if (bos != null) {
+        try {
+          bos.close();
+        } catch (Exception e) {
+          // ignore
+        }
+      }
+      screenshot.recycle();
+    }
+  }
+
+  protected abstract Bitmap takeScreenshot();
+
+  protected abstract DroidDriverContext<?, ?> getContext();
+}
diff --git a/src/com/google/android/droiddriver/base/BaseUiElement.java b/src/com/google/android/droiddriver/base/BaseUiElement.java
new file mode 100644
index 0000000..3122ba3
--- /dev/null
+++ b/src/com/google/android/droiddriver/base/BaseUiElement.java
@@ -0,0 +1,302 @@
+/*
+ * 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.base;
+
+import android.graphics.Rect;
+
+import com.google.android.droiddriver.UiElement;
+import com.google.android.droiddriver.actions.Action;
+import com.google.android.droiddriver.actions.EventUiElementActor;
+import com.google.android.droiddriver.actions.UiElementActor;
+import com.google.android.droiddriver.exceptions.DroidDriverException;
+import com.google.android.droiddriver.finders.Attribute;
+import com.google.android.droiddriver.finders.Predicate;
+import com.google.android.droiddriver.finders.Predicates;
+import com.google.android.droiddriver.scroll.Direction.PhysicalDirection;
+import com.google.android.droiddriver.util.Logs;
+import com.google.android.droiddriver.util.Strings;
+import com.google.android.droiddriver.util.Strings.ToStringHelper;
+import com.google.android.droiddriver.validators.Validator;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+
+/**
+ * Base UiElement that implements the common operations.
+ *
+ * @param <R> the type of the raw element this class wraps, for example, View or
+ *        AccessibilityNodeInfo
+ * @param <E> the type of the concrete subclass of BaseUiElement
+ */
+public abstract class BaseUiElement<R, E extends BaseUiElement<R, E>> implements UiElement {
+  // These two attribute names are used for debugging only.
+  // The two constants are used internally and must match to-uiautomator.xsl.
+  public static final String ATTRIB_VISIBLE_BOUNDS = "VisibleBounds";
+  public static final String ATTRIB_NOT_VISIBLE = "NotVisible";
+
+  private UiElementActor uiElementActor = EventUiElementActor.INSTANCE;
+  private Validator validator = null;
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public <T> T get(Attribute attribute) {
+    return (T) getAttributes().get(attribute);
+  }
+
+  @Override
+  public String getText() {
+    return get(Attribute.TEXT);
+  }
+
+  @Override
+  public String getContentDescription() {
+    return get(Attribute.CONTENT_DESC);
+  }
+
+  @Override
+  public String getClassName() {
+    return get(Attribute.CLASS);
+  }
+
+  @Override
+  public String getResourceId() {
+    return get(Attribute.RESOURCE_ID);
+  }
+
+  @Override
+  public String getPackageName() {
+    return get(Attribute.PACKAGE);
+  }
+
+  @Override
+  public boolean isCheckable() {
+    return (Boolean) get(Attribute.CHECKABLE);
+  }
+
+  @Override
+  public boolean isChecked() {
+    return (Boolean) get(Attribute.CHECKED);
+  }
+
+  @Override
+  public boolean isClickable() {
+    return (Boolean) get(Attribute.CLICKABLE);
+  }
+
+  @Override
+  public boolean isEnabled() {
+    return (Boolean) get(Attribute.ENABLED);
+  }
+
+  @Override
+  public boolean isFocusable() {
+    return (Boolean) get(Attribute.FOCUSABLE);
+  }
+
+  @Override
+  public boolean isFocused() {
+    return (Boolean) get(Attribute.FOCUSED);
+  }
+
+  @Override
+  public boolean isScrollable() {
+    return (Boolean) get(Attribute.SCROLLABLE);
+  }
+
+  @Override
+  public boolean isLongClickable() {
+    return (Boolean) get(Attribute.LONG_CLICKABLE);
+  }
+
+  @Override
+  public boolean isPassword() {
+    return (Boolean) get(Attribute.PASSWORD);
+  }
+
+  @Override
+  public boolean isSelected() {
+    return (Boolean) get(Attribute.SELECTED);
+  }
+
+  @Override
+  public Rect getBounds() {
+    return get(Attribute.BOUNDS);
+  }
+
+  // TODO: expose these 3 methods in UiElement?
+  public int getSelectionStart() {
+    Integer value = get(Attribute.SELECTION_START);
+    return value == null ? 0 : value;
+  }
+
+  public int getSelectionEnd() {
+    Integer value = get(Attribute.SELECTION_END);
+    return value == null ? 0 : value;
+  }
+
+  public boolean hasSelection() {
+    final int selectionStart = getSelectionStart();
+    final int selectionEnd = getSelectionEnd();
+
+    return selectionStart >= 0 && selectionStart != selectionEnd;
+  }
+
+  @Override
+  public boolean perform(Action action) {
+    Logs.call(this, "perform", action);
+    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);
+  }
+
+  protected boolean doPerform(Action action) {
+    return action.perform(this);
+  }
+
+  protected abstract void doPerformAndWait(FutureTask<Boolean> futureTask, long timeoutMillis);
+
+  private boolean performAndWait(final Action action) {
+    FutureTask<Boolean> futureTask = new FutureTask<Boolean>(new Callable<Boolean>() {
+      @Override
+      public Boolean call() {
+        return doPerform(action);
+      }
+    });
+    doPerformAndWait(futureTask, action.getTimeoutMillis());
+
+    try {
+      return futureTask.get();
+    } catch (ExecutionException e) {
+      Throwable cause = e.getCause();
+      if (cause instanceof RuntimeException) {
+        throw (RuntimeException) cause;
+      }
+      throw new DroidDriverException(cause);
+    } catch (InterruptedException e) {
+      throw new DroidDriverException(e);
+    }
+  }
+
+  @Override
+  public void setText(String text) {
+    uiElementActor.setText(this, text);
+  }
+
+  @Override
+  public void click() {
+    uiElementActor.click(this);
+  }
+
+  @Override
+  public void longClick() {
+    uiElementActor.longClick(this);
+  }
+
+  @Override
+  public void doubleClick() {
+    uiElementActor.doubleClick(this);
+  }
+
+  @Override
+  public void scroll(PhysicalDirection direction) {
+    uiElementActor.scroll(this, direction);
+  }
+
+  protected abstract Map<Attribute, Object> getAttributes();
+
+  protected abstract List<E> getChildren();
+
+  @Override
+  public List<E> getChildren(Predicate<? super UiElement> predicate) {
+    List<E> children = getChildren();
+    if (children == null) {
+      return Collections.emptyList();
+    }
+    if (predicate == null || predicate.equals(Predicates.any())) {
+      return children;
+    }
+
+    List<E> filteredChildren = new ArrayList<E>(children.size());
+    for (E child : children) {
+      if (predicate.apply(child)) {
+        filteredChildren.add(child);
+      }
+    }
+    return Collections.unmodifiableList(filteredChildren);
+  }
+
+  @Override
+  public String toString() {
+    ToStringHelper toStringHelper = Strings.toStringHelper(this);
+    for (Map.Entry<Attribute, Object> entry : getAttributes().entrySet()) {
+      addAttribute(toStringHelper, entry.getKey(), entry.getValue());
+    }
+    if (!isVisible()) {
+      toStringHelper.addValue(ATTRIB_NOT_VISIBLE);
+    } else if (!getVisibleBounds().equals(getBounds())) {
+      toStringHelper.add(ATTRIB_VISIBLE_BOUNDS, getVisibleBounds().toShortString());
+    }
+    return toStringHelper.toString();
+  }
+
+  private static void addAttribute(ToStringHelper toStringHelper, Attribute attr, Object value) {
+    if (value != null) {
+      if (value instanceof Boolean) {
+        if ((Boolean) value) {
+          toStringHelper.addValue(attr.getName());
+        }
+      } else if (value instanceof Rect) {
+        toStringHelper.add(attr.getName(), ((Rect) value).toShortString());
+      } else {
+        toStringHelper.add(attr.getName(), value);
+      }
+    }
+  }
+
+  /**
+   * Gets the raw element used to create this UiElement. The attributes of this
+   * UiElement are based on a snapshot of the raw element at construction time.
+   * If the raw element is updated later, the attributes may not match.
+   */
+  // TODO: expose in UiElement?
+  public abstract R getRawElement();
+
+  public void setUiElementActor(UiElementActor uiElementActor) {
+    this.uiElementActor = uiElementActor;
+  }
+
+  /**
+   * 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/util/DefaultPoller.java b/src/com/google/android/droiddriver/base/DefaultPoller.java
similarity index 88%
rename from src/com/google/android/droiddriver/util/DefaultPoller.java
rename to src/com/google/android/droiddriver/base/DefaultPoller.java
index 842f121..c6d69c5 100644
--- a/src/com/google/android/droiddriver/util/DefaultPoller.java
+++ b/src/com/google/android/droiddriver/base/DefaultPoller.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.google.android.droiddriver.util;
+package com.google.android.droiddriver.base;
 
 import android.os.SystemClock;
 
@@ -22,17 +22,16 @@
 import com.google.android.droiddriver.Poller;
 import com.google.android.droiddriver.exceptions.TimeoutException;
 import com.google.android.droiddriver.finders.Finder;
-import com.google.common.collect.Lists;
-import com.google.common.primitives.Longs;
 
 import java.util.Collection;
+import java.util.LinkedList;
 
 /**
  * Default implementation of a {@link Poller}.
  */
 public class DefaultPoller implements Poller {
-  private final Collection<TimeoutListener> timeoutListeners = Lists.newLinkedList();
-  private final Collection<PollingListener> pollingListeners = Lists.newLinkedList();
+  private final Collection<TimeoutListener> timeoutListeners = new LinkedList<TimeoutListener>();
+  private final Collection<PollingListener> pollingListeners = new LinkedList<PollingListener>();
   private long timeoutMillis = 10000;
   private long intervalMillis = 500;
 
@@ -67,11 +66,16 @@
     long end = SystemClock.uptimeMillis() + timeoutMillis;
     while (true) {
       try {
+        driver.refreshUiElementTree();
         return checker.check(driver, finder);
       } catch (UnsatisfiedConditionException e) {
         // fall through to poll
       }
 
+      for (PollingListener pollingListener : pollingListeners) {
+        pollingListener.onPolling(driver, finder);
+      }
+
       long remainingMillis = end - SystemClock.uptimeMillis();
       if (remainingMillis < 0) {
         for (TimeoutListener timeoutListener : timeoutListeners) {
@@ -80,11 +84,7 @@
         throw new TimeoutException(String.format(
             "Timed out after %d milliseconds waiting for %s %s", timeoutMillis, finder, checker));
       }
-
-      for (PollingListener pollingListener : pollingListeners) {
-        pollingListener.onPolling(driver, finder);
-      }
-      SystemClock.sleep(Longs.min(intervalMillis, remainingMillis));
+      SystemClock.sleep(Math.min(intervalMillis, remainingMillis));
     }
   }
 
diff --git a/src/com/google/android/droiddriver/base/DroidDriverContext.java b/src/com/google/android/droiddriver/base/DroidDriverContext.java
new file mode 100644
index 0000000..a306bc0
--- /dev/null
+++ b/src/com/google/android/droiddriver/base/DroidDriverContext.java
@@ -0,0 +1,148 @@
+/*
+ * 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.base;
+
+import android.app.Instrumentation;
+import android.os.Looper;
+import android.util.Log;
+
+import com.google.android.droiddriver.exceptions.DroidDriverException;
+import com.google.android.droiddriver.exceptions.TimeoutException;
+import com.google.android.droiddriver.finders.ByXPath;
+import com.google.android.droiddriver.util.Logs;
+
+import java.util.Map;
+import java.util.WeakHashMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Internal helper for DroidDriver implementation.
+ */
+public class DroidDriverContext<R, E extends BaseUiElement<R, E>> {
+  private final Instrumentation instrumentation;
+  private final BaseDroidDriver<R, E> driver;
+  private final Map<R, E> map;
+
+  public DroidDriverContext(Instrumentation instrumentation, BaseDroidDriver<R, E> driver) {
+    this.instrumentation = instrumentation;
+    this.driver = driver;
+    map = new WeakHashMap<R, E>();
+  }
+
+  public Instrumentation getInstrumentation() {
+    return instrumentation;
+  }
+
+  public BaseDroidDriver<R, E> getDriver() {
+    return driver;
+  }
+
+  public E getElement(R rawElement, E parent) {
+    E element = map.get(rawElement);
+    if (element == null) {
+      element = driver.newUiElement(rawElement, parent);
+      map.put(rawElement, element);
+    }
+    return element;
+  }
+
+  public E newRootElement(R rawRoot) {
+    clearData();
+    return getElement(rawRoot, null /* parent */);
+  }
+
+  private void clearData() {
+    map.clear();
+    ByXPath.clearData();
+  }
+
+  /**
+   * Tries to wait for an idle state on the main thread on best-effort basis up
+   * to {@code timeoutMillis}. The main thread may not enter the idle state when
+   * animation is playing, for example, the ProgressBar.
+   */
+  public boolean tryWaitForIdleSync(long timeoutMillis) {
+    validateNotAppThread();
+    FutureTask<?> futureTask = new FutureTask<Void>(new Runnable() {
+      @Override
+      public void run() {}
+    }, null);
+    instrumentation.waitForIdle(futureTask);
+
+    try {
+      futureTask.get(timeoutMillis, TimeUnit.MILLISECONDS);
+    } catch (InterruptedException e) {
+      throw new DroidDriverException(e);
+    } catch (ExecutionException e) {
+      throw new DroidDriverException(e);
+    } catch (java.util.concurrent.TimeoutException e) {
+      Logs.log(Log.DEBUG, String.format(
+          "Timed out after %d milliseconds waiting for idle on main looper", timeoutMillis));
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * Tries to run {@code runnable} on the main thread on best-effort basis up to
+   * {@code timeoutMillis}. The {@code runnable} may never run, for example, in
+   * case that the main Looper has exited due to uncaught exception.
+   */
+  public boolean tryRunOnMainSync(Runnable runnable, long timeoutMillis) {
+    validateNotAppThread();
+    final FutureTask<?> futureTask = new FutureTask<Void>(runnable, null);
+    new Thread(new Runnable() {
+      @Override
+      public void run() {
+        instrumentation.runOnMainSync(futureTask);
+      }
+    }).start();
+
+    try {
+      futureTask.get(timeoutMillis, TimeUnit.MILLISECONDS);
+    } catch (InterruptedException e) {
+      throw new DroidDriverException(e);
+    } catch (ExecutionException e) {
+      throw new DroidDriverException(e);
+    } catch (java.util.concurrent.TimeoutException e) {
+      Logs.log(Log.WARN, getRunOnMainSyncTimeoutMessage(timeoutMillis));
+      return false;
+    }
+    return true;
+  }
+
+  public void runOnMainSync(Runnable runnable) {
+    long timeoutMillis = getDriver().getPoller().getTimeoutMillis();
+    if (!tryRunOnMainSync(runnable, timeoutMillis)) {
+      throw new TimeoutException(getRunOnMainSyncTimeoutMessage(timeoutMillis));
+    }
+  }
+
+  private String getRunOnMainSyncTimeoutMessage(long timeoutMillis) {
+    return String.format(
+        "Timed out after %d milliseconds waiting for Instrumentation.runOnMainSync", timeoutMillis);
+  }
+
+  private void validateNotAppThread() {
+    if (Looper.myLooper() == Looper.getMainLooper()) {
+      throw new DroidDriverException(
+          "This method can not be called from the main application thread");
+    }
+  }
+}
diff --git a/src/com/google/android/droiddriver/exceptions/UnrecoverableException.java b/src/com/google/android/droiddriver/exceptions/UnrecoverableException.java
new file mode 100644
index 0000000..c3b069f
--- /dev/null
+++ b/src/com/google/android/droiddriver/exceptions/UnrecoverableException.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.exceptions;
+
+import com.google.android.droiddriver.helpers.BaseDroidDriverTest;
+
+/**
+ * When an {@link UnrecoverableException} occurs, the rest of the tests are
+ * going to fail as well, therefore running them only adds noise to the report.
+ * {@link BaseDroidDriverTest} will skip remaining tests when this is thrown.
+ */
+@SuppressWarnings("serial")
+public class UnrecoverableException extends RuntimeException {
+  public UnrecoverableException(String message) {
+    super(message);
+  }
+
+  public UnrecoverableException(Throwable throwable) {
+    super(throwable);
+  }
+}
diff --git a/src/com/google/android/droiddriver/finders/Attribute.java b/src/com/google/android/droiddriver/finders/Attribute.java
index 2f8bbf6..9799117 100644
--- a/src/com/google/android/droiddriver/finders/Attribute.java
+++ b/src/com/google/android/droiddriver/finders/Attribute.java
@@ -16,124 +16,25 @@
 
 package com.google.android.droiddriver.finders;
 
-import android.graphics.Rect;
-
-import com.google.android.droiddriver.UiElement;
-
 public enum Attribute {
-  CHECKABLE("checkable") {
-    @SuppressWarnings("unchecked")
-    @Override
-    public Boolean getValue(UiElement element) {
-      return element.isCheckable();
-    }
-  },
-  CHECKED("checked") {
-    @SuppressWarnings("unchecked")
-    @Override
-    public Boolean getValue(UiElement element) {
-      return element.isChecked();
-    }
-  },
-  CLASS("class") {
-    @SuppressWarnings("unchecked")
-    @Override
-    public String getValue(UiElement element) {
-      return element.getClassName();
-    }
-  },
-  CLICKABLE("clickable") {
-    @SuppressWarnings("unchecked")
-    @Override
-    public Boolean getValue(UiElement element) {
-      return element.isClickable();
-    }
-  },
-  CONTENT_DESC("content-desc") {
-    @SuppressWarnings("unchecked")
-    @Override
-    public String getValue(UiElement element) {
-      return element.getContentDescription();
-    }
-  },
-  ENABLED("enabled") {
-    @SuppressWarnings("unchecked")
-    @Override
-    public Boolean getValue(UiElement element) {
-      return element.isEnabled();
-    }
-  },
-  FOCUSABLE("focusable") {
-    @SuppressWarnings("unchecked")
-    @Override
-    public Boolean getValue(UiElement element) {
-      return element.isFocusable();
-    }
-  },
-  FOCUSED("focused") {
-    @SuppressWarnings("unchecked")
-    @Override
-    public Boolean getValue(UiElement element) {
-      return element.isFocused();
-    }
-  },
-  LONG_CLICKABLE("long-clickable") {
-    @SuppressWarnings("unchecked")
-    @Override
-    public Boolean getValue(UiElement element) {
-      return element.isLongClickable();
-    }
-  },
-  PACKAGE("package") {
-    @SuppressWarnings("unchecked")
-    @Override
-    public String getValue(UiElement element) {
-      return element.getPackageName();
-    }
-  },
-  PASSWORD("password") {
-    @SuppressWarnings("unchecked")
-    @Override
-    public Boolean getValue(UiElement element) {
-      return element.isPassword();
-    }
-  },
-  RESOURCE_ID("resource-id") {
-    @SuppressWarnings("unchecked")
-    @Override
-    public String getValue(UiElement element) {
-      return element.getResourceId();
-    }
-  },
-  SCROLLABLE("scrollable") {
-    @SuppressWarnings("unchecked")
-    @Override
-    public Boolean getValue(UiElement element) {
-      return element.isScrollable();
-    }
-  },
-  SELECTED("selected") {
-    @SuppressWarnings("unchecked")
-    @Override
-    public Boolean getValue(UiElement element) {
-      return element.isSelected();
-    }
-  },
-  TEXT("text") {
-    @SuppressWarnings("unchecked")
-    @Override
-    public String getValue(UiElement element) {
-      return element.getText();
-    }
-  },
-  BOUNDS("bounds") {
-    @SuppressWarnings("unchecked")
-    @Override
-    public Rect getValue(UiElement element) {
-      // TODO: clip by boundsInParent?
-      return element.getBounds();
-    }
-  };
+  CHECKABLE("checkable"),
+  CHECKED("checked"),
+  CLASS("class"),
+  CLICKABLE("clickable"),
+  CONTENT_DESC("content-desc"),
+  ENABLED("enabled"),
+  FOCUSABLE("focusable"),
+  FOCUSED("focused"),
+  LONG_CLICKABLE("long-clickable"),
+  PACKAGE("package"),
+  PASSWORD("password"),
+  RESOURCE_ID("resource-id"),
+  SCROLLABLE("scrollable"),
+  SELECTION_START("selection-start"),
+  SELECTION_END("selection-end"),
+  SELECTED("selected"),
+  TEXT("text"),
+  BOUNDS("bounds");
 
   private final String name;
 
@@ -145,8 +46,6 @@
     return name;
   }
 
-  public abstract <T> T getValue(UiElement element);
-
   @Override
   public String toString() {
     return name;
diff --git a/src/com/google/android/droiddriver/finders/By.java b/src/com/google/android/droiddriver/finders/By.java
index 69e7c73..642b03d 100644
--- a/src/com/google/android/droiddriver/finders/By.java
+++ b/src/com/google/android/droiddriver/finders/By.java
@@ -16,161 +16,81 @@
 
 package com.google.android.droiddriver.finders;
 
-import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.android.droiddriver.util.Preconditions.checkNotNull;
 
 import com.google.android.droiddriver.UiElement;
-import com.google.common.annotations.Beta;
-import com.google.common.base.Joiner;
-import com.google.common.base.Objects;
+import com.google.android.droiddriver.exceptions.ElementNotFoundException;
 
 /**
  * Convenience methods to create commonly used finders.
  */
 public class By {
-  /** Matches by {@link Object#equals}. */
-  public static final MatchStrategy<Object> OBJECT_EQUALS = new MatchStrategy<Object>() {
-    @Override
-    public boolean match(Object expected, Object actual) {
-      return Objects.equal(actual, expected);
-    }
+  private static final MatchFinder ANY = new MatchFinder(null);
 
-    @Override
-    public String toString() {
-      return "equals";
-    }
-  };
-  /** Matches by {@link String#matches}. */
-  public static final MatchStrategy<String> STRING_MATCHES = new MatchStrategy<String>() {
-    @Override
-    public boolean match(String expected, String actual) {
-      return actual != null && actual.matches(expected);
-    }
-
-    @Override
-    public String toString() {
-      return "matches pattern";
-    }
-  };
-  /** Matches by {@link String#contains}. */
-  public static final MatchStrategy<String> STRING_CONTAINS = new MatchStrategy<String>() {
-    @Override
-    public boolean match(String expected, String actual) {
-      return actual != null && actual.contains(expected);
-    }
-
-    @Override
-    public String toString() {
-      return "contains";
-    }
-  };
-
-  /**
-   * Creates a new ByAttribute finder. Frequently-used finders have shorthands
-   * below, for example, {@link #text}, {@link #textRegex}. Users can create
-   * custom finders using this method.
-   *
-   * @param attribute the attribute to match against
-   * @param strategy the matching strategy, for instance, equals or matches
-   *        regular expression
-   * @param expected the expected attribute value
-   * @return a new ByAttribute finder
-   */
-  public static <T> ByAttribute<T> attribute(Attribute attribute,
-      MatchStrategy<? super T> strategy, T expected) {
-    return new ByAttribute<T>(attribute, strategy, expected);
+  /** Matches any UiElement. */
+  public static MatchFinder any() {
+    return ANY;
   }
 
-  /** Shorthand for {@link #attribute}{@code (attribute, OBJECT_EQUALS, expected)} */
-  public static <T> ByAttribute<T> attribute(Attribute attribute, T expected) {
-    return attribute(attribute, OBJECT_EQUALS, expected);
-  }
-
-  /** Shorthand for {@link #attribute}{@code (attribute, true)} */
-  public static ByAttribute<Boolean> is(Attribute attribute) {
-    return attribute(attribute, true);
-  }
-
-  /** Shorthand for {@link #attribute}{@code (attribute, false)} */
-  public static ByAttribute<Boolean> not(Attribute attribute) {
-    return attribute(attribute, false);
+  /** Matches a UiElement whose {@code attribute} is {@code true}. */
+  public static MatchFinder is(Attribute attribute) {
+    return new MatchFinder(Predicates.attributeTrue(attribute));
   }
 
   /**
-   * @param resourceId The resource id to match against
-   * @return a finder to find an element by resource id
+   * Matches a UiElement whose {@code attribute} is {@code false} or is not set.
    */
-  public static ByAttribute<String> resourceId(String resourceId) {
-    return attribute(Attribute.RESOURCE_ID, OBJECT_EQUALS, resourceId);
+  public static MatchFinder not(Attribute attribute) {
+    return new MatchFinder(Predicates.attributeFalse(attribute));
   }
 
-  /**
-   * @param name The exact package name to match against
-   * @return a finder to find an element by package name
-   */
-  public static ByAttribute<String> packageName(String name) {
-    return attribute(Attribute.PACKAGE, OBJECT_EQUALS, name);
+  /** Matches a UiElement by resource id. */
+  public static MatchFinder resourceId(String resourceId) {
+    return new MatchFinder(Predicates.attributeEquals(Attribute.RESOURCE_ID, resourceId));
   }
 
-  /**
-   * @param text The exact text to match against
-   * @return a finder to find an element by text
-   */
-  public static ByAttribute<String> text(String text) {
-    return attribute(Attribute.TEXT, OBJECT_EQUALS, text);
+  /** Matches a UiElement by package name. */
+  public static MatchFinder packageName(String name) {
+    return new MatchFinder(Predicates.attributeEquals(Attribute.PACKAGE, name));
   }
 
-  /**
-   * @param regex The regular expression pattern to match against
-   * @return a finder to find an element by text pattern
-   */
-  public static ByAttribute<String> textRegex(String regex) {
-    return attribute(Attribute.TEXT, STRING_MATCHES, regex);
+  /** Matches a UiElement by the exact text. */
+  public static MatchFinder text(String text) {
+    return new MatchFinder(Predicates.attributeEquals(Attribute.TEXT, text));
   }
 
-  /**
-   * @param substring String inside a text field
-   * @return a finder to find an element by text substring
-   */
-  public static ByAttribute<String> textContains(String substring) {
-    return attribute(Attribute.TEXT, STRING_CONTAINS, substring);
+  /** Matches a UiElement whose text matches {@code regex}. */
+  public static MatchFinder textRegex(String regex) {
+    return new MatchFinder(Predicates.attributeMatches(Attribute.TEXT, regex));
   }
 
-  /**
-   * @param contentDescription The exact content description to match against
-   * @return a finder to find an element by content description
-   */
-  public static ByAttribute<String> contentDescription(String contentDescription) {
-    return attribute(Attribute.CONTENT_DESC, OBJECT_EQUALS, contentDescription);
+  /** Matches a UiElement whose text contains {@code substring}. */
+  public static MatchFinder textContains(String substring) {
+    return new MatchFinder(Predicates.attributeContains(Attribute.TEXT, substring));
   }
 
-  /**
-   * @param substring String inside a content description
-   * @return a finder to find an element by content description substring
-   */
-  public static ByAttribute<String> contentDescriptionContains(String substring) {
-    return attribute(Attribute.CONTENT_DESC, STRING_CONTAINS, substring);
+  /** Matches a UiElement by content description. */
+  public static MatchFinder contentDescription(String contentDescription) {
+    return new MatchFinder(Predicates.attributeEquals(Attribute.CONTENT_DESC, contentDescription));
   }
 
-  /**
-   * @param className The exact class name to match against
-   * @return a finder to find an element by class name
-   */
-  public static ByAttribute<String> className(String className) {
-    return attribute(Attribute.CLASS, OBJECT_EQUALS, className);
+  /** Matches a UiElement whose content description contains {@code substring}. */
+  public static MatchFinder contentDescriptionContains(String substring) {
+    return new MatchFinder(Predicates.attributeContains(Attribute.CONTENT_DESC, substring));
   }
 
-  /**
-   * @param clazz The class whose name is matched against
-   * @return a finder to find an element by class name
-   */
-  public static ByAttribute<String> className(Class<?> clazz) {
+  /** Matches a UiElement by class name. */
+  public static MatchFinder className(String className) {
+    return new MatchFinder(Predicates.attributeEquals(Attribute.CLASS, className));
+  }
+
+  /** Matches a UiElement by class name. */
+  public static MatchFinder className(Class<?> clazz) {
     return className(clazz.getName());
   }
 
-  /**
-   * @return a finder to find an element that is selected
-   */
-  public static ByAttribute<Boolean> selected() {
+  /** Matches a UiElement that is selected. */
+  public static MatchFinder selected() {
     return is(Attribute.SELECTED);
   }
 
@@ -210,20 +130,34 @@
    * @param xPath The xpath to use
    * @return a finder which locates elements via XPath
    */
-  @Beta
   public static ByXPath xpath(String xPath) {
     return new ByXPath(xPath);
   }
 
   /**
-   * @return a finder that uses the UiElement returned by parent Finder as
-   *         context for the child Finder
+   * Returns a finder that uses the UiElement returned by first Finder as
+   * context for the second Finder.
+   * <p>
+   * typically first Finder finds the ancestor, then second Finder finds the
+   * target UiElement, which is a descendant.
+   * </p>
+   * Note that if the first Finder matches multiple UiElements, only the first
+   * match is tried, which usually is not what callers expect. In this case,
+   * allOf(second, withAncesor(first)) may work.
    */
-  public static ChainFinder chain(Finder parent, Finder child) {
-    return new ChainFinder(parent, child);
+  public static ChainFinder chain(Finder first, Finder second) {
+    return new ChainFinder(first, second);
   }
 
-  // Hamcrest style finder aggregators
+  private static Predicate<? super UiElement>[] getPredicates(MatchFinder... finders) {
+    @SuppressWarnings("unchecked")
+    Predicate<? super UiElement>[] predicates = new Predicate[finders.length];
+    for (int i = 0; i < finders.length; i++) {
+      predicates[i] = finders[i].predicate;
+    }
+    return predicates;
+  }
+
   /**
    * Evaluates given {@finders} in short-circuit fashion in the order
    * they are passed. Costly finders (for example those returned by with*
@@ -233,22 +167,7 @@
    * @return a finder that is the logical conjunction of given finders
    */
   public static MatchFinder allOf(final MatchFinder... finders) {
-    return new MatchFinder() {
-      @Override
-      public boolean matches(UiElement element) {
-        for (MatchFinder finder : finders) {
-          if (!finder.matches(element)) {
-            return false;
-          }
-        }
-        return true;
-      }
-
-      @Override
-      public String toString() {
-        return "allOf(" + Joiner.on(",").join(finders) + ")";
-      }
-    };
+    return new MatchFinder(Predicates.allOf(getPredicates(finders)));
   }
 
   /**
@@ -260,96 +179,73 @@
    * @return a finder that is the logical disjunction of given finders
    */
   public static MatchFinder anyOf(final MatchFinder... finders) {
-    return new MatchFinder() {
-      @Override
-      public boolean matches(UiElement element) {
-        for (MatchFinder finder : finders) {
-          if (finder.matches(element)) {
-            return true;
-          }
-        }
-        return false;
-      }
-
-      @Override
-      public String toString() {
-        return "anyOf(" + Joiner.on(",").join(finders) + ")";
-      }
-    };
+    return new MatchFinder(Predicates.anyOf(getPredicates(finders)));
   }
 
   /**
    * Matches a UiElement whose parent matches the given parentFinder. For
    * complex cases, consider {@link #xpath}.
    */
-  public static MatchFinder withParent(final MatchFinder parentFinder) {
+  public static MatchFinder withParent(MatchFinder parentFinder) {
     checkNotNull(parentFinder);
-    return new MatchFinder() {
-      @Override
-      public boolean matches(UiElement element) {
-        UiElement parent = element.getParent();
-        return parent != null && parentFinder.matches(parent);
-      }
-
-      @Override
-      public String toString() {
-        return "withParent(" + parentFinder + ")";
-      }
-    };
+    return new MatchFinder(Predicates.withParent(parentFinder.predicate));
   }
 
   /**
    * Matches a UiElement whose ancestor matches the given ancestorFinder. For
    * complex cases, consider {@link #xpath}.
    */
-  public static MatchFinder withAncestor(final MatchFinder ancestorFinder) {
+  public static MatchFinder withAncestor(MatchFinder ancestorFinder) {
     checkNotNull(ancestorFinder);
-    return new MatchFinder() {
-      @Override
-      public boolean matches(UiElement element) {
-        UiElement parent = element.getParent();
-        while (parent != null) {
-          if (ancestorFinder.matches(parent)) {
-            return true;
-          }
-          parent = parent.getParent();
-        }
-        return false;
-      }
-
-      @Override
-      public String toString() {
-        return "withAncestor(" + ancestorFinder + ")";
-      }
-    };
+    return new MatchFinder(Predicates.withAncestor(ancestorFinder.predicate));
   }
 
   /**
-   * Matches a UiElement which has a sibling matching the given siblingFinder.
-   * For complex cases, consider {@link #xpath}.
+   * Matches a UiElement which has a visible sibling matching the given
+   * siblingFinder. This could be inefficient; consider {@link #xpath}.
    */
-  public static MatchFinder withSibling(final MatchFinder siblingFinder) {
+  public static MatchFinder withSibling(MatchFinder siblingFinder) {
     checkNotNull(siblingFinder);
-    return new MatchFinder() {
+    return new MatchFinder(Predicates.withSibling(siblingFinder.predicate));
+  }
+
+  /**
+   * Matches a UiElement which has a visible child matching the given
+   * childFinder. This could be inefficient; consider {@link #xpath}.
+   */
+  public static MatchFinder withChild(MatchFinder childFinder) {
+    checkNotNull(childFinder);
+    return new MatchFinder(Predicates.withChild(childFinder.predicate));
+  }
+
+  /**
+   * Matches a UiElement whose descendant (including self) matches the given
+   * descendantFinder. This could be VERY inefficient; consider {@link #xpath}.
+   */
+  public static MatchFinder withDescendant(final MatchFinder descendantFinder) {
+    checkNotNull(descendantFinder);
+    return new MatchFinder(new Predicate<UiElement>() {
       @Override
-      public boolean matches(UiElement element) {
-        UiElement parent = element.getParent();
-        if (parent == null) {
+      public boolean apply(UiElement element) {
+        try {
+          descendantFinder.find(element);
+          return true;
+        } catch (ElementNotFoundException enfe) {
           return false;
         }
-        for (int i = 0; i < parent.getChildCount(); i++) {
-          if (siblingFinder.matches(parent.getChild(i))) {
-            return true;
-          }
-        }
-        return false;
       }
 
       @Override
       public String toString() {
-        return "withSibling(" + siblingFinder + ")";
+        return "withDescendant(" + descendantFinder + ")";
       }
-    };
+    });
+  }
+
+  /** Matches a UiElement that does not match the provided {@code finder}. */
+  public static MatchFinder not(MatchFinder finder) {
+    checkNotNull(finder);
+    return new MatchFinder(Predicates.not(finder.predicate));
   }
 
   private By() {}
diff --git a/src/com/google/android/droiddriver/finders/ByAttribute.java b/src/com/google/android/droiddriver/finders/ByAttribute.java
deleted file mode 100644
index d7d8689..0000000
--- a/src/com/google/android/droiddriver/finders/ByAttribute.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * 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.finders;
-
-import static com.google.common.base.Preconditions.checkNotNull;
-
-import com.google.android.droiddriver.UiElement;
-
-/**
- * Matches UiElement by a single attribute.
- */
-public class ByAttribute<T> extends MatchFinder {
-  private final Attribute attribute;
-  private final MatchStrategy<? super T> strategy;
-  private final T expected;
-
-  protected ByAttribute(Attribute attribute, MatchStrategy<? super T> strategy, T expected) {
-    this.attribute = checkNotNull(attribute);
-    this.strategy = checkNotNull(strategy);
-    this.expected = checkNotNull(expected);
-  }
-
-  @Override
-  public boolean matches(UiElement element) {
-    T value = attribute.getValue(element);
-    return strategy.match(expected, value);
-  }
-
-  @Override
-  public String toString() {
-    return String.format("ByAttribute{%s %s %s}", attribute, strategy, expected);
-  }
-}
diff --git a/src/com/google/android/droiddriver/finders/ByXPath.java b/src/com/google/android/droiddriver/finders/ByXPath.java
index 87ba192..946261a 100644
--- a/src/com/google/android/droiddriver/finders/ByXPath.java
+++ b/src/com/google/android/droiddriver/finders/ByXPath.java
@@ -18,19 +18,21 @@
 import android.util.Log;
 
 import com.google.android.droiddriver.UiElement;
-import com.google.android.droiddriver.base.AbstractUiElement;
+import com.google.android.droiddriver.base.BaseUiElement;
 import com.google.android.droiddriver.exceptions.DroidDriverException;
 import com.google.android.droiddriver.exceptions.ElementNotFoundException;
 import com.google.android.droiddriver.util.FileUtils;
 import com.google.android.droiddriver.util.Logs;
-import com.google.common.base.Objects;
-import com.google.common.base.Preconditions;
+import com.google.android.droiddriver.util.Preconditions;
+import com.google.android.droiddriver.util.Strings;
 
 import org.w3c.dom.DOMException;
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 
 import java.io.BufferedOutputStream;
+import java.util.HashMap;
+import java.util.Map;
 
 import javax.xml.parsers.DocumentBuilderFactory;
 import javax.xml.parsers.ParserConfigurationException;
@@ -50,10 +52,21 @@
  */
 public class ByXPath implements Finder {
   private static final XPath XPATH_COMPILER = XPathFactory.newInstance().newXPath();
-  private static final String UI_ELEMENT = "UiElement";
   // document needs to be static so that when buildDomNode is called recursively
   // on children they are in the same document to be appended.
   private static Document document;
+  // The two maps should be kept in sync
+  private static final Map<BaseUiElement<?, ?>, Element> TO_DOM_MAP =
+      new HashMap<BaseUiElement<?, ?>, Element>();
+  private static final Map<Element, BaseUiElement<?, ?>> FROM_DOM_MAP =
+      new HashMap<Element, BaseUiElement<?, ?>>();
+
+  public static void clearData() {
+    TO_DOM_MAP.clear();
+    FROM_DOM_MAP.clear();
+    document = null;
+  }
+
   private final String xPathString;
   private final XPathExpression xPathExpression;
 
@@ -68,12 +81,12 @@
 
   @Override
   public String toString() {
-    return Objects.toStringHelper(this).addValue(xPathString).toString();
+    return Strings.toStringHelper(this).addValue(xPathString).toString();
   }
 
   @Override
   public UiElement find(UiElement context) {
-    Element domNode = ((AbstractUiElement) context).getDomNode();
+    Element domNode = getDomNode((BaseUiElement<?, ?>) context, UiElement.VISIBLE);
     try {
       getDocument().appendChild(domNode);
       Element foundNode = (Element) xPathExpression.evaluate(domNode, XPathConstants.NODE);
@@ -82,7 +95,7 @@
         throw new ElementNotFoundException(this);
       }
 
-      UiElement match = (UiElement) foundNode.getUserData(UI_ELEMENT);
+      UiElement match = FROM_DOM_MAP.get(foundNode);
       Logs.log(Log.INFO, "Found match: " + match);
       return match;
     } catch (XPathExpressionException e) {
@@ -109,15 +122,26 @@
   }
 
   /**
-   * Used internally in {@link AbstractUiElement}.
+   * Returns the DOM node representing this UiElement.
    */
-  public static Element buildDomNode(AbstractUiElement uiElement) {
+  private static Element getDomNode(BaseUiElement<?, ?> uiElement,
+      Predicate<? super UiElement> predicate) {
+    Element domNode = TO_DOM_MAP.get(uiElement);
+    if (domNode == null) {
+      domNode = buildDomNode(uiElement, predicate);
+    }
+    return domNode;
+  }
+
+  private static Element buildDomNode(BaseUiElement<?, ?> uiElement,
+      Predicate<? super UiElement> predicate) {
     String className = uiElement.getClassName();
     if (className == null) {
       className = "UNKNOWN";
     }
     Element element = getDocument().createElement(XPaths.tag(className));
-    element.setUserData(UI_ELEMENT, uiElement, null /* UserDataHandler */);
+    TO_DOM_MAP.put(uiElement, element);
+    FROM_DOM_MAP.put(element, uiElement);
 
     setAttribute(element, Attribute.CLASS, className);
     setAttribute(element, Attribute.RESOURCE_ID, uiElement.getResourceId());
@@ -133,23 +157,27 @@
     setAttribute(element, Attribute.SCROLLABLE, uiElement.isScrollable());
     setAttribute(element, Attribute.LONG_CLICKABLE, uiElement.isLongClickable());
     setAttribute(element, Attribute.PASSWORD, uiElement.isPassword());
+    if (uiElement.hasSelection()) {
+      element.setAttribute(Attribute.SELECTION_START.getName(),
+          Integer.toString(uiElement.getSelectionStart()));
+      element.setAttribute(Attribute.SELECTION_END.getName(),
+          Integer.toString(uiElement.getSelectionEnd()));
+    }
     setAttribute(element, Attribute.SELECTED, uiElement.isSelected());
     element.setAttribute(Attribute.BOUNDS.getName(), uiElement.getBounds().toShortString());
 
-    // TODO: visitor pattern
-    int childCount = uiElement.getChildCount();
-    for (int i = 0; i < childCount; i++) {
-      AbstractUiElement child = uiElement.getChild(i);
-      if (child == null) {
-        Logs.log(Log.INFO, "Skip null child for " + uiElement);
-        continue;
+    // If we're dumping for debugging, add extra information
+    if (!UiElement.VISIBLE.equals(predicate)) {
+      if (!uiElement.isVisible()) {
+        element.setAttribute(BaseUiElement.ATTRIB_NOT_VISIBLE, "");
+      } else if (!uiElement.getVisibleBounds().equals(uiElement.getBounds())) {
+        element.setAttribute(BaseUiElement.ATTRIB_VISIBLE_BOUNDS, uiElement.getVisibleBounds()
+            .toShortString());
       }
-      if (!child.isVisible()) {
-        Logs.log(Log.VERBOSE, "Skip invisible child: " + child);
-        continue;
-      }
+    }
 
-      element.appendChild(child.getDomNode());
+    for (BaseUiElement<?, ?> child : uiElement.getChildren(predicate)) {
+      element.appendChild(getDomNode(child, predicate));
     }
     return element;
   }
@@ -167,18 +195,24 @@
     }
   }
 
-  public static boolean dumpDom(String path, AbstractUiElement uiElement) {
+  public static boolean dumpDom(String path, BaseUiElement<?, ?> uiElement) {
     BufferedOutputStream bos = null;
     try {
       bos = FileUtils.open(path);
       Transformer transformer = TransformerFactory.newInstance().newTransformer();
       transformer.setOutputProperty(OutputKeys.INDENT, "yes");
-      transformer.transform(new DOMSource(uiElement.getDomNode()), new StreamResult(bos));
+      // find() filters invisible UiElements, but this is for debugging and
+      // invisible UiElements may be of interest.
+      clearData();
+      Element domNode = getDomNode(uiElement, null);
+      transformer.transform(new DOMSource(domNode), new StreamResult(bos));
       Logs.log(Log.INFO, "Wrote dom to " + path);
     } catch (Exception e) {
       Logs.log(Log.ERROR, e, "Failed to transform node");
       return false;
     } finally {
+      // We built DOM with invisible UiElements. Don't use it for find()!
+      clearData();
       if (bos != null) {
         try {
           bos.close();
diff --git a/src/com/google/android/droiddriver/finders/ChainFinder.java b/src/com/google/android/droiddriver/finders/ChainFinder.java
index 244c167..6e42903 100644
--- a/src/com/google/android/droiddriver/finders/ChainFinder.java
+++ b/src/com/google/android/droiddriver/finders/ChainFinder.java
@@ -17,28 +17,35 @@
 package com.google.android.droiddriver.finders;
 
 import com.google.android.droiddriver.UiElement;
-import com.google.common.base.Preconditions;
+import com.google.android.droiddriver.util.Preconditions;
 
 /**
- * Find UiElement by using the UiElement returned by parent Finder as context
- * for the child Finder.
+ * Finds UiElement by applying Finders in turn: using the UiElement returned by
+ * first Finder as context for the second Finder. It is conceptually similar to
+ * <a href="http://en.wikipedia.org/wiki/Functional_composition">Function
+ * composition</a>. The returned UiElement can be thought of as the result of
+ * second(first(context)).
+ * <p>
+ * Note typically first Finder finds the ancestor, then second Finder finds the
+ * target UiElement, which is a descendant. ChainFinder can be chained with
+ * additional Finders to make a "chain".
  */
 public class ChainFinder implements Finder {
-  private final Finder parent;
-  private final Finder child;
+  private final Finder first;
+  private final Finder second;
 
-  protected ChainFinder(Finder parent, Finder child) {
-    this.parent = Preconditions.checkNotNull(parent);
-    this.child = Preconditions.checkNotNull(child);
+  protected ChainFinder(Finder first, Finder second) {
+    this.first = Preconditions.checkNotNull(first);
+    this.second = Preconditions.checkNotNull(second);
   }
 
   @Override
   public String toString() {
-    return String.format("ChainFinder{%s, %s}", parent, child);
+    return String.format("Chain{%s, %s}", first, second);
   }
 
   @Override
   public UiElement find(UiElement context) {
-    return child.find(parent.find(context));
+    return second.find(first.find(context));
   }
 }
diff --git a/src/com/google/android/droiddriver/finders/Finder.java b/src/com/google/android/droiddriver/finders/Finder.java
index 884b4f4..9a7dd1d 100644
--- a/src/com/google/android/droiddriver/finders/Finder.java
+++ b/src/com/google/android/droiddriver/finders/Finder.java
@@ -39,7 +39,7 @@
    *
    * <p>
    * It is recommended that this method return the description of the finder,
-   * for example, "ByAttribute{text equals OK}".
+   * for example, "{text=OK}".
    */
   @Override
   String toString();
diff --git a/src/com/google/android/droiddriver/finders/MatchFinder.java b/src/com/google/android/droiddriver/finders/MatchFinder.java
index 22f3fc2..df92f72 100644
--- a/src/com/google/android/droiddriver/finders/MatchFinder.java
+++ b/src/com/google/android/droiddriver/finders/MatchFinder.java
@@ -24,27 +24,23 @@
 
 /**
  * Traverses the UiElement tree and returns the first UiElement satisfying
- * {@link #matches(UiElement)}.
+ * {@link #predicate}.
  */
-public abstract class MatchFinder implements Finder {
-  /**
-   * Returns true if the {@code element} matches the implementing finder. The
-   * implementing finder should not poll.
-   *
-   * @param element The element to validate against
-   * @return true if the element matches
-   */
-  public abstract boolean matches(UiElement element);
+public class MatchFinder implements Finder {
+  protected final Predicate<? super UiElement> predicate;
 
-  /**
-   * {@inheritDoc}
-   *
-   * <p>
-   * It is recommended that this method return the description of the finder,
-   * for example, "ByAttribute{text equals OK}".
-   */
+  public MatchFinder(Predicate<? super UiElement> predicate) {
+    if (predicate == null) {
+      this.predicate = Predicates.any();
+    } else {
+      this.predicate = predicate;
+    }
+  }
+
   @Override
-  public abstract String toString();
+  public String toString() {
+    return predicate.toString();
+  }
 
   @Override
   public UiElement find(UiElement context) {
@@ -52,17 +48,7 @@
       Logs.log(Log.INFO, "Found match: " + context);
       return context;
     }
-    int childCount = context.getChildCount();
-    for (int i = 0; i < childCount; i++) {
-      UiElement child = context.getChild(i);
-      if (child == null) {
-        Logs.log(Log.INFO, "Skip null child for " + context);
-        continue;
-      }
-      if (!child.isVisible()) {
-        Logs.log(Log.VERBOSE, "Skip invisible child: " + child);
-        continue;
-      }
+    for (UiElement child : context.getChildren(UiElement.VISIBLE)) {
       try {
         return find(child);
       } catch (ElementNotFoundException enfe) {
@@ -71,4 +57,16 @@
     }
     throw new ElementNotFoundException(this);
   }
+
+  /**
+   * Returns true if the {@code element} matches this finder. This can be used
+   * to test the exact match of {@code element} when this finder is used in
+   * {@link By#anyOf(MatchFinder...)}.
+   *
+   * @param element The element to validate against
+   * @return true if the element matches
+   */
+  public final boolean matches(UiElement element) {
+    return predicate.apply(element);
+  }
 }
diff --git a/src/com/google/android/droiddriver/finders/MatchStrategy.java b/src/com/google/android/droiddriver/finders/MatchStrategy.java
deleted file mode 100644
index 089f70b..0000000
--- a/src/com/google/android/droiddriver/finders/MatchStrategy.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * 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.finders;
-
-/**
- * Encapsulates the matching strategy.
- */
-public interface MatchStrategy<T> {
-  /**
-   * @return true if actual matches expected
-   */
-  boolean match(T expected, T actual);
-
-  /**
-   * {@inheritDoc}
-   *
-   * <p>
-   * It is recommended that this method return the description of the matching
-   * strategy, for example, "matches pattern".
-   */
-  @Override
-  String toString();
-}
diff --git a/src/com/google/android/droiddriver/finders/Predicate.java b/src/com/google/android/droiddriver/finders/Predicate.java
new file mode 100644
index 0000000..18b3e66
--- /dev/null
+++ b/src/com/google/android/droiddriver/finders/Predicate.java
@@ -0,0 +1,49 @@
+/*
+ * 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.finders;
+
+/**
+ * Determines a true or false value for a given input.
+ *
+ * This is replicated from the open-source <a
+ * href="http://guava-libraries.googlecode.com">Guava libraries</a>.
+ * <p>
+ * Many apps use Guava. If a test apk also contains a copy of Guava, duplicated
+ * classes in app and test apks may cause error at run-time:
+ * "Class ref in pre-verified class resolved to unexpected implementation". To
+ * simplify the build and deployment set-up, DroidDriver copies the code of some
+ * Guava classes (often simplified) to this package such that it does not depend
+ * on Guava.
+ * </p>
+ */
+public interface Predicate<T> {
+  /**
+   * Returns the result of applying this predicate to {@code input}.
+   */
+  boolean apply(T input);
+
+  /**
+   * {@inheritDoc}
+   *
+   * <p>
+   * It is recommended that this method return the description of this
+   * Predicate.
+   * </p>
+   */
+  @Override
+  String toString();
+}
diff --git a/src/com/google/android/droiddriver/finders/Predicates.java b/src/com/google/android/droiddriver/finders/Predicates.java
new file mode 100644
index 0000000..36d2fa3
--- /dev/null
+++ b/src/com/google/android/droiddriver/finders/Predicates.java
@@ -0,0 +1,321 @@
+/*
+ * 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.finders;
+
+import android.text.TextUtils;
+
+import com.google.android.droiddriver.UiElement;
+
+/**
+ * Static utility methods pertaining to {@code Predicate} instances.
+ */
+public final class Predicates {
+  private Predicates() {}
+
+  private static final Predicate<Object> ANY = new Predicate<Object>() {
+    @Override
+    public boolean apply(Object o) {
+      return true;
+    }
+
+    @Override
+    public String toString() {
+      return "any";
+    }
+  };
+
+  /**
+   * Returns a predicate that always evaluates to {@code true}.
+   */
+  @SuppressWarnings("unchecked")
+  public static <T> Predicate<T> any() {
+    return (Predicate<T>) ANY;
+  }
+
+  /**
+   * Returns a predicate that is the negation of the provided {@code predicate}.
+   */
+  public static <T> Predicate<T> not(final Predicate<T> predicate) {
+    return new Predicate<T>() {
+      @Override
+      public boolean apply(T input) {
+        return !predicate.apply(input);
+      }
+
+      @Override
+      public String toString() {
+        return "not(" + predicate + ")";
+      }
+    };
+  }
+
+  /**
+   * Returns a predicate that evaluates to {@code true} if both arguments
+   * evaluate to {@code true}. The arguments are evaluated in order, and
+   * evaluation will be "short-circuited" as soon as a false predicate is found.
+   */
+  @SuppressWarnings("unchecked")
+  public static <T> Predicate<T> allOf(final Predicate<? super T> first,
+      final Predicate<? super T> second) {
+    if (first == null || first == ANY) {
+      return (Predicate<T>) second;
+    }
+    if (second == null || second == ANY) {
+      return (Predicate<T>) first;
+    }
+
+    return new Predicate<T>() {
+      @Override
+      public boolean apply(T input) {
+        return first.apply(input) && second.apply(input);
+      }
+
+      @Override
+      public String toString() {
+        return "allOf(" + first + ", " + second + ")";
+      }
+    };
+  }
+
+  /**
+   * Returns a predicate that evaluates to {@code true} if each of its
+   * components evaluates to {@code true}. The components are evaluated in
+   * order, and evaluation will be "short-circuited" as soon as a false
+   * predicate is found.
+   */
+  public static <T> Predicate<T> allOf(
+      @SuppressWarnings("unchecked") final Predicate<? super T>... components) {
+    return new Predicate<T>() {
+      @Override
+      public boolean apply(T input) {
+        for (Predicate<? super T> each : components) {
+          if (!each.apply(input)) {
+            return false;
+          }
+        }
+        return true;
+      }
+
+      @Override
+      public String toString() {
+        return "allOf(" + TextUtils.join(", ", components) + ")";
+      }
+    };
+  }
+
+  /**
+   * Returns a predicate that evaluates to {@code true} if any one of its
+   * components evaluates to {@code true}. The components are evaluated in
+   * order, and evaluation will be "short-circuited" as soon as a true predicate
+   * is found.
+   */
+  public static <T> Predicate<T> anyOf(
+      @SuppressWarnings("unchecked") final Predicate<? super T>... components) {
+    return new Predicate<T>() {
+      @Override
+      public boolean apply(T input) {
+        for (Predicate<? super T> each : components) {
+          if (each.apply(input)) {
+            return true;
+          }
+        }
+        return false;
+      }
+
+      @Override
+      public String toString() {
+        return "anyOf(" + TextUtils.join(", ", components) + ")";
+      }
+    };
+  }
+
+  /**
+   * Returns a predicate that evaluates to {@code true} on a {@link UiElement}
+   * if its {@code attribute} is {@code true}.
+   */
+  public static Predicate<UiElement> attributeTrue(final Attribute attribute) {
+    return new Predicate<UiElement>() {
+      @Override
+      public boolean apply(UiElement element) {
+        Boolean actual = element.get(attribute);
+        return actual != null && actual;
+      }
+
+      @Override
+      public String toString() {
+        return String.format("{%s}", attribute);
+      }
+    };
+  }
+
+  /**
+   * Returns a predicate that evaluates to {@code true} on a {@link UiElement}
+   * if its {@code attribute} is {@code false}.
+   */
+  public static Predicate<UiElement> attributeFalse(final Attribute attribute) {
+    return new Predicate<UiElement>() {
+      @Override
+      public boolean apply(UiElement element) {
+        Boolean actual = element.get(attribute);
+        return actual == null || !actual;
+      }
+
+      @Override
+      public String toString() {
+        return String.format("{not %s}", attribute);
+      }
+    };
+  }
+
+  /**
+   * Returns a predicate that evaluates to {@code true} on a {@link UiElement}
+   * if its {@code attribute} equals {@code expected}.
+   */
+  public static Predicate<UiElement> attributeEquals(final Attribute attribute,
+      final Object expected) {
+    return new Predicate<UiElement>() {
+      @Override
+      public boolean apply(UiElement element) {
+        Object actual = element.get(attribute);
+        return actual == expected || (actual != null && actual.equals(expected));
+      }
+
+      @Override
+      public String toString() {
+        return String.format("{%s=%s}", attribute, expected);
+      }
+    };
+  }
+
+  /**
+   * Returns a predicate that evaluates to {@code true} on a {@link UiElement}
+   * if its {@code attribute} matches {@code regex}.
+   */
+  public static Predicate<UiElement> attributeMatches(final Attribute attribute, final String regex) {
+    return new Predicate<UiElement>() {
+      @Override
+      public boolean apply(UiElement element) {
+        String actual = element.get(attribute);
+        return actual != null && actual.matches(regex);
+      }
+
+      @Override
+      public String toString() {
+        return String.format("{%s matches %s}", attribute, regex);
+      }
+    };
+  }
+
+  /**
+   * Returns a predicate that evaluates to {@code true} on a {@link UiElement}
+   * if its {@code attribute} contains {@code substring}.
+   */
+  public static Predicate<UiElement> attributeContains(final Attribute attribute,
+      final String substring) {
+    return new Predicate<UiElement>() {
+      @Override
+      public boolean apply(UiElement element) {
+        String actual = element.get(attribute);
+        return actual != null && actual.contains(substring);
+      }
+
+      @Override
+      public String toString() {
+        return String.format("{%s contains %s}", attribute, substring);
+      }
+    };
+  }
+
+  public static Predicate<UiElement> withParent(final Predicate<? super UiElement> parentPredicate) {
+    return new Predicate<UiElement>() {
+      @Override
+      public boolean apply(UiElement element) {
+        UiElement parent = element.getParent();
+        return parent != null && parentPredicate.apply(parent);
+      }
+
+      @Override
+      public String toString() {
+        return "withParent(" + parentPredicate + ")";
+      }
+    };
+  }
+
+  public static Predicate<UiElement> withAncestor(
+      final Predicate<? super UiElement> ancestorPredicate) {
+    return new Predicate<UiElement>() {
+      @Override
+      public boolean apply(UiElement element) {
+        UiElement parent = element.getParent();
+        while (parent != null) {
+          if (ancestorPredicate.apply(parent)) {
+            return true;
+          }
+          parent = parent.getParent();
+        }
+        return false;
+      }
+
+      @Override
+      public String toString() {
+        return "withAncestor(" + ancestorPredicate + ")";
+      }
+    };
+  }
+
+  public static Predicate<UiElement> withSibling(final Predicate<? super UiElement> siblingPredicate) {
+    return new Predicate<UiElement>() {
+      @Override
+      public boolean apply(UiElement element) {
+        UiElement parent = element.getParent();
+        if (parent == null) {
+          return false;
+        }
+        for (UiElement sibling : parent.getChildren(UiElement.VISIBLE)) {
+          if (sibling != element && siblingPredicate.apply(sibling)) {
+            return true;
+          }
+        }
+        return false;
+      }
+
+      @Override
+      public String toString() {
+        return "withSibling(" + siblingPredicate + ")";
+      }
+    };
+  }
+
+  public static Predicate<UiElement> withChild(final Predicate<? super UiElement> childPredicate) {
+    return new Predicate<UiElement>() {
+      @Override
+      public boolean apply(UiElement element) {
+        for (UiElement child : element.getChildren(UiElement.VISIBLE)) {
+          if (childPredicate.apply(child)) {
+            return true;
+          }
+        }
+        return false;
+      }
+
+      @Override
+      public String toString() {
+        return "withChild(" + childPredicate + ")";
+      }
+    };
+  }
+}
diff --git a/src/com/google/android/droiddriver/finders/XPaths.java b/src/com/google/android/droiddriver/finders/XPaths.java
index c401582..ef97d2d 100644
--- a/src/com/google/android/droiddriver/finders/XPaths.java
+++ b/src/com/google/android/droiddriver/finders/XPaths.java
@@ -16,8 +16,7 @@
 
 package com.google.android.droiddriver.finders;
 
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Joiner;
+import android.text.TextUtils;
 
 /**
  * Convenience methods and constants for XPath.
@@ -44,7 +43,7 @@
    *         this to build XPath instead of String literals.
    */
   public static String tag(Class<?> clazz) {
-    return tag(clazz.getName());
+    return tag(clazz.getSimpleName());
   }
 
   private static String simpleClassName(String name) {
@@ -95,6 +94,19 @@
     return attr(Attribute.TEXT, value);
   }
 
+  /** Shorthand for {@link #attr}{@code (Attribute.RESOURCE_ID, value)} */
+  public static String resourceId(String value) {
+    return attr(Attribute.RESOURCE_ID, value);
+  }
+
+  /**
+   * @return XPath predicate (with enclosing []) that filters nodes with
+   *         descendants satisfying {@code descendantPredicate}.
+   */
+  public static String withDescendant(String descendantPredicate) {
+    return "[.//*" + descendantPredicate + "]";
+  }
+
   /**
    * Adapted from http://stackoverflow.com/questions/1341847/.
    * <p>
@@ -103,7 +115,6 @@
    * produce very long XPath expressions if a value contains a long run of
    * double quotes.
    */
-  @VisibleForTesting
   static String quoteXPathLiteral(String value) {
     // if the value contains only single or double quotes, construct an XPath
     // literal
@@ -120,7 +131,7 @@
     // concat("foo", '"', "bar")
     StringBuilder sb = new StringBuilder();
     sb.append("concat(\"");
-    Joiner.on("\",'\"',\"").appendTo(sb, value.split("\""));
+    sb.append(TextUtils.join("\",'\"',\"", value.split("\"")));
     sb.append("\")");
     return sb.toString();
   }
diff --git a/src/com/google/android/droiddriver/helpers/BaseDroidDriverTest.java b/src/com/google/android/droiddriver/helpers/BaseDroidDriverTest.java
new file mode 100644
index 0000000..656d0f3
--- /dev/null
+++ b/src/com/google/android/droiddriver/helpers/BaseDroidDriverTest.java
@@ -0,0 +1,245 @@
+/*
+ * 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.helpers;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.Debug;
+import android.test.FlakyTest;
+import android.util.Log;
+
+import com.google.android.droiddriver.DroidDriver;
+import com.google.android.droiddriver.exceptions.UnrecoverableException;
+import com.google.android.droiddriver.util.FileUtils;
+import com.google.android.droiddriver.util.Logs;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.lang.Thread.UncaughtExceptionHandler;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+
+/**
+ * Base class for tests using DroidDriver that handles uncaught exceptions, for
+ * example OOME, and takes screenshot on failure. It is NOT required, but
+ * provides handy utility methods.
+ */
+public abstract class BaseDroidDriverTest<T extends Activity> extends
+    D2ActivityInstrumentationTestCase2<T> {
+  private static boolean classSetUpDone = false;
+  // In case of device-wide fatal errors, e.g. OOME, the remaining tests will
+  // fail and the messages will not help, so skip them.
+  private static boolean skipRemainingTests = false;
+  // Prevent crash by uncaught exception.
+  private static volatile Throwable uncaughtException;
+  static {
+    Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() {
+      @Override
+      public void uncaughtException(Thread thread, Throwable ex) {
+        uncaughtException = ex;
+        // In most cases uncaughtException will be reported by onFailure().
+        // But if it occurs in InstrumentationTestRunner, it's swallowed.
+        // Always log it for all cases.
+        Logs.log(Log.ERROR, uncaughtException, "uncaughtException");
+      }
+    });
+  }
+
+  protected DroidDriver driver;
+
+  protected BaseDroidDriverTest(Class<T> activityClass) {
+    super(activityClass);
+  }
+
+  @Override
+  protected void setUp() throws Exception {
+    super.setUp();
+    if (!classSetUpDone) {
+      classSetUp();
+      classSetUpDone = true;
+    }
+    driver = DroidDrivers.get();
+  }
+
+  @Override
+  protected void tearDown() throws Exception {
+    super.tearDown();
+    driver = null;
+  }
+
+  protected Context getTargetContext() {
+    return getInstrumentation().getTargetContext();
+  }
+
+  /**
+   * Initializes test fixture once for all tests extending this class. Typically
+   * you call {@link DroidDrivers#init} with an appropriate instance. If an
+   * InstrumentationDriver is used, this is a good place to call
+   * {@link com.google.android.droiddriver.instrumentation.ViewElement#overrideClassName}
+   */
+  protected abstract void classSetUp();
+
+  /**
+   * Takes a screenshot on failure.
+   */
+  protected void onFailure(Throwable failure) throws Throwable {
+    // If skipRemainingTests is true, the failure has already been reported.
+    if (skipRemainingTests) {
+      return;
+    }
+    if (shouldSkipRemainingTests(failure)) {
+      skipRemainingTests = true;
+    }
+
+    // Give uncaughtException (thrown by app instead of tests) high priority
+    if (uncaughtException != null) {
+      failure = uncaughtException;
+    }
+
+    try {
+      if (failure instanceof OutOfMemoryError) {
+        dumpHprof();
+      } else if (uncaughtException == null) {
+        String baseFileName = getBaseFileName();
+        driver.dumpUiElementTree(baseFileName + ".xml");
+        driver.getUiDevice().takeScreenshot(baseFileName + ".png");
+      }
+    } catch (Throwable e) {
+      // This method is for troubleshooting. Do not throw new error; we'll
+      // throw the original failure.
+      Logs.log(Log.WARN, e);
+      if (e instanceof OutOfMemoryError && !(failure instanceof OutOfMemoryError)) {
+        skipRemainingTests = true;
+        dumpHprof();
+      }
+    }
+
+    throw failure;
+  }
+
+  protected boolean shouldSkipRemainingTests(Throwable e) {
+    return e instanceof UnrecoverableException || e instanceof OutOfMemoryError
+        || skipRemainingTests || uncaughtException != null;
+  }
+
+  /**
+   * Gets the base filename for troubleshooting files. For example, a screenshot
+   * is saved in the file "basename".png.
+   */
+  protected String getBaseFileName() {
+    return "dd/" + getClass().getSimpleName() + "." + getName();
+  }
+
+  protected void dumpHprof() throws IOException, FileNotFoundException {
+    String path = FileUtils.getAbsoluteFile(getBaseFileName() + ".hprof").getPath();
+    // create an empty readable file
+    FileUtils.open(path).close();
+    Debug.dumpHprofData(path);
+  }
+
+  /**
+   * Fixes JUnit3: always call tearDown even when setUp throws. Also calls
+   * {@link #onFailure}.
+   */
+  @Override
+  public void runBare() throws Throwable {
+    if (skipRemainingTests) {
+      return;
+    }
+    if (uncaughtException != null) {
+      onFailure(uncaughtException);
+    }
+
+    Throwable exception = null;
+    try {
+      setUp();
+      runTest();
+    } catch (Throwable runException) {
+      exception = runException;
+      // ActivityInstrumentationTestCase2.tearDown() finishes activity
+      // created by getActivity(), so call this before tearDown().
+      onFailure(exception);
+    } finally {
+      try {
+        tearDown();
+      } catch (Throwable tearDownException) {
+        if (exception == null) {
+          exception = tearDownException;
+        }
+      }
+    }
+    if (exception != null) {
+      throw exception;
+    }
+  }
+
+  /**
+   * Overrides super.runTest() to fail fast when the test is annotated as
+   * FlakyTest and we should skip remaining tests (the failure is fatal).
+   * When a flaky test is re-run, tearDown() and setUp() are called first in order
+   * to reset the test's state.
+   */
+  @Override
+  protected void runTest() throws Throwable {
+    String fName = getName();
+    assertNotNull(fName);
+    Method method = null;
+    try {
+      // use getMethod to get all public inherited
+      // methods. getDeclaredMethods returns all
+      // methods of this class but excludes the
+      // inherited ones.
+      method = getClass().getMethod(fName, (Class[]) null);
+    } catch (NoSuchMethodException e) {
+      fail("Method \"" + fName + "\" not found");
+    }
+
+    if (!Modifier.isPublic(method.getModifiers())) {
+      fail("Method \"" + fName + "\" should be public");
+    }
+
+    int tolerance = 1;
+    if (method.isAnnotationPresent(FlakyTest.class)) {
+      tolerance = method.getAnnotation(FlakyTest.class).tolerance();
+    }
+
+    for (int runCount = 0; runCount < tolerance; runCount++) {
+      if (runCount > 0) {
+        Logs.logfmt(Log.INFO, "Running %s round %d of %d attempts", fName, runCount + 1, tolerance);
+        // We are re-attempting a test, so reset all state.
+        tearDown();
+        setUp();
+      }
+
+      try {
+        method.invoke(this);
+        return;
+      } catch (InvocationTargetException e) {
+        e.fillInStackTrace();
+        Throwable exception = e.getTargetException();
+        if (shouldSkipRemainingTests(exception) || runCount >= tolerance - 1) {
+          throw exception;
+        }
+        Logs.log(Log.WARN, exception);
+      } catch (IllegalAccessException e) {
+        e.fillInStackTrace();
+        throw e;
+      }
+    }
+  }
+}
diff --git a/src/com/google/android/droiddriver/helpers/D2ActivityInstrumentationTestCase2.java b/src/com/google/android/droiddriver/helpers/D2ActivityInstrumentationTestCase2.java
new file mode 100644
index 0000000..ec5ea9f
--- /dev/null
+++ b/src/com/google/android/droiddriver/helpers/D2ActivityInstrumentationTestCase2.java
@@ -0,0 +1,68 @@
+/*
+ * 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.helpers;
+
+import android.app.Activity;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.ActivityTestCase;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+
+/**
+ * Fixes bugs in {@link ActivityInstrumentationTestCase2}.
+ */
+public abstract class D2ActivityInstrumentationTestCase2<T extends Activity> extends
+    ActivityInstrumentationTestCase2<T> {
+  protected D2ActivityInstrumentationTestCase2(Class<T> activityClass) {
+    super(activityClass);
+  }
+
+  /**
+   * Fixes a bug in {@link ActivityTestCase#scrubClass} that causes
+   * NullPointerException if your leaf-level test class declares static fields.
+   * This is <a href="https://code.google.com/p/android/issues/detail?id=4244">a
+   * known bug</a> that has been fixed in ICS Android release. But it still
+   * exists on devices older than ICS. If your test class extends this class, it
+   * can work on older devices.
+   * <p>
+   * In addition to the official fix in ICS and beyond, which skips
+   * {@code final} fields, the fix below also skips {@code static} fields, which
+   * should be the expectation of Java programmers.
+   * </p>
+   */
+  @Override
+  protected void scrubClass(final Class<?> testCaseClass) throws IllegalAccessException {
+    final Field[] fields = getClass().getDeclaredFields();
+    for (Field field : fields) {
+      final Class<?> fieldClass = field.getDeclaringClass();
+      if (testCaseClass.isAssignableFrom(fieldClass) && !field.getType().isPrimitive()
+          && !Modifier.isFinal(field.getModifiers()) && !Modifier.isStatic(field.getModifiers())) {
+        try {
+          field.setAccessible(true);
+          field.set(this, null);
+        } catch (Exception e) {
+          android.util.Log.d("TestCase", "Error: Could not nullify field!");
+        }
+
+        if (field.get(this) != null) {
+          android.util.Log.d("TestCase", "Error: Could not nullify field!");
+        }
+      }
+    }
+  }
+}
diff --git a/src/com/google/android/droiddriver/helpers/DroidDrivers.java b/src/com/google/android/droiddriver/helpers/DroidDrivers.java
new file mode 100644
index 0000000..c281075
--- /dev/null
+++ b/src/com/google/android/droiddriver/helpers/DroidDrivers.java
@@ -0,0 +1,149 @@
+/*
+ * 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.helpers;
+
+import android.app.Instrumentation;
+import android.os.Build;
+import android.os.Bundle;
+
+import com.google.android.droiddriver.DroidDriver;
+import com.google.android.droiddriver.exceptions.DroidDriverException;
+import com.google.android.droiddriver.instrumentation.InstrumentationDriver;
+import com.google.android.droiddriver.uiautomation.UiAutomationDriver;
+
+import java.lang.reflect.InvocationTargetException;
+
+/**
+ * Static utility methods pertaining to {@link DroidDriver} instances.
+ */
+public class DroidDrivers {
+  private static DroidDriver driver;
+  private static Instrumentation instrumentation;
+  private static Bundle options;
+
+  /**
+   * Gets the singleton driver. Throws if {@link #init} has not been called.
+   */
+  public static DroidDriver get() {
+    if (DroidDrivers.driver == null) {
+      throw new DroidDriverException("init() has not been called");
+    }
+    return DroidDrivers.driver;
+  }
+
+  /**
+   * Initializes the singleton driver. The singleton driver is NOT required, but
+   * it is handy and using a singleton driver can avoid memory leak if you have
+   * many instances around (for example, one in every test -- JUnit framework
+   * keeps the test instances in memory after running them).
+   */
+  public static void init(DroidDriver driver) {
+    if (DroidDrivers.driver != null) {
+      throw new DroidDriverException("init() can only be called once");
+    }
+    DroidDrivers.driver = driver;
+  }
+
+  /**
+   * Initializes for the convenience methods {@link #getInstrumentation()} and
+   * {@link #getOptions()}. Called by
+   * {@link com.google.android.droiddriver.runner.TestRunner}. If a custom
+   * runner is used, this method must be called appropriately, otherwise the two
+   * convenience methods won't work.
+   */
+  public static void initInstrumentation(Instrumentation instrumentation, Bundle arguments) {
+    if (DroidDrivers.instrumentation != null) {
+      throw new DroidDriverException("DroidDrivers.initInstrumentation() can only be called once");
+    }
+    DroidDrivers.instrumentation = instrumentation;
+    DroidDrivers.options = arguments;
+  }
+
+  public static Instrumentation getInstrumentation() {
+    return instrumentation;
+  }
+
+  /**
+   * Gets the <a href=
+   * "http://developer.android.com/tools/testing/testing_otheride.html#AMOptionsSyntax"
+   * >am instrument options</a>.
+   */
+  public static Bundle getOptions() {
+    return options;
+  }
+
+  /**
+   * Returns whether the running target (device or emulator) has
+   * {@link android.app.UiAutomation} API, which is introduced in SDK API 18
+   * (JELLY_BEAN_MR2).
+   */
+  public static boolean hasUiAutomation() {
+    return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2;
+  }
+
+  /**
+   * Returns a new DroidDriver instance. If am instrument options have "driver",
+   * treat it as the fully-qualified-class-name and create a new instance of it
+   * with {@code instrumentation} as the argument; otherwise a new
+   * platform-dependent default DroidDriver instance.
+   */
+  public static DroidDriver newDriver(Instrumentation instrumentation) {
+    String driverClass = options == null ? null : options.getString("driver");
+    if (driverClass != null) {
+      try {
+        return (DroidDriver) Class.forName(driverClass).getConstructor(Instrumentation.class)
+            .newInstance(instrumentation);
+      } catch (ClassNotFoundException e) {
+        throw new DroidDriverException(e);
+      } catch (NoSuchMethodException e) {
+        throw new DroidDriverException(e);
+      } catch (InstantiationException e) {
+        throw new DroidDriverException(e);
+      } catch (IllegalAccessException e) {
+        throw new DroidDriverException(e);
+      } catch (IllegalArgumentException e) {
+        throw new DroidDriverException(e);
+      } catch (InvocationTargetException e) {
+        throw new DroidDriverException(e);
+      }
+    }
+
+    // If "driver" is not specified, return default.
+    if (hasUiAutomation()) {
+      return newUiAutomationDriver(instrumentation);
+    }
+    return newInstrumentationDriver(instrumentation);
+  }
+
+  /** Returns a new InstrumentationDriver */
+  public static InstrumentationDriver newInstrumentationDriver(Instrumentation instrumentation) {
+    return new InstrumentationDriver(instrumentation);
+  }
+
+  /** Returns a new UiAutomationDriver */
+  public static UiAutomationDriver newUiAutomationDriver(Instrumentation instrumentation) {
+    if (!hasUiAutomation()) {
+      throw new DroidDriverException("UiAutomation is not available below API 18. "
+          + "See http://developer.android.com/reference/android/app/UiAutomation.html");
+    }
+    if (instrumentation.getUiAutomation() == null) {
+      throw new DroidDriverException(
+          "uiAutomation==null: did you forget to set '-w' flag for 'am instrument'?");
+    }
+    return new UiAutomationDriver(instrumentation);
+  }
+}
diff --git a/src/com/google/android/droiddriver/helpers/PollingListeners.java b/src/com/google/android/droiddriver/helpers/PollingListeners.java
new file mode 100644
index 0000000..44cd826
--- /dev/null
+++ b/src/com/google/android/droiddriver/helpers/PollingListeners.java
@@ -0,0 +1,55 @@
+package com.google.android.droiddriver.helpers;
+
+import com.google.android.droiddriver.DroidDriver;
+import com.google.android.droiddriver.Poller.PollingListener;
+import com.google.android.droiddriver.exceptions.ElementNotFoundException;
+import com.google.android.droiddriver.finders.Finder;
+
+/**
+ * Static utility methods to create commonly used PollingListeners.
+ */
+public class PollingListeners {
+  /**
+   * Tries to find {@code watchFinder}, and clicks it if found.
+   *
+   * @param watchFinder Identifies the UI component to watch
+   * @return whether {@code watchFinder} is found
+   */
+  public static boolean tryFindAndClick(DroidDriver driver, Finder watchFinder) {
+    try {
+      driver.find(watchFinder).click();
+      return true;
+    } catch (ElementNotFoundException enfe) {
+      return false;
+    }
+  }
+
+  /**
+   * Returns a new {@code PollingListener} that will look for
+   * {@code watchFinder}, then click {@code dismissFinder} to dismiss it.
+   * <p>
+   * Typically a {@code PollingListener} is used to dismiss "random" dialogs. If
+   * you know the certain situation when a dialog is displayed, you should deal
+   * with the dialog in the specific situation instead of using a
+   * {@code PollingListener} because it is checked in all polling events, which
+   * occur frequently.
+   * </p>
+   *
+   * @param watchFinder Identifies the UI component, for example an AlertDialog
+   * @param dismissFinder Identifies the UiElement to click on that will dismiss
+   *        the UI component
+   */
+  public static PollingListener newDismissListener(final Finder watchFinder,
+      final Finder dismissFinder) {
+    return new PollingListener() {
+      @Override
+      public void onPolling(DroidDriver driver, Finder finder) {
+        if (driver.has(watchFinder)) {
+          driver.find(dismissFinder).click();
+        }
+      }
+    };
+  }
+
+  private PollingListeners() {}
+}
diff --git a/src/com/google/android/droiddriver/helpers/ScrollerHelper.java b/src/com/google/android/droiddriver/helpers/ScrollerHelper.java
new file mode 100644
index 0000000..74059ad
--- /dev/null
+++ b/src/com/google/android/droiddriver/helpers/ScrollerHelper.java
@@ -0,0 +1,81 @@
+/*
+ * 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.helpers;
+
+import com.google.android.droiddriver.DroidDriver;
+import com.google.android.droiddriver.UiElement;
+import com.google.android.droiddriver.exceptions.ElementNotFoundException;
+import com.google.android.droiddriver.finders.Finder;
+import com.google.android.droiddriver.scroll.Direction.PhysicalDirection;
+import com.google.android.droiddriver.scroll.Scroller;
+
+/**
+ * Helper for Scroller.
+ */
+public class ScrollerHelper {
+  private final DroidDriver driver;
+  private final Finder containerFinder;
+  private final Scroller scroller;
+
+  public ScrollerHelper(Scroller scroller, DroidDriver driver, Finder containerFinder) {
+    this.scroller = scroller;
+    this.driver = driver;
+    this.containerFinder = containerFinder;
+  }
+
+  /**
+   * Scrolls {@code containerFinder} in both directions if necessary to find
+   * {@code itemFinder}, which is a descendant of {@code containerFinder}.
+   *
+   * @param itemFinder Finder for the desired item; relative to
+   *        {@code containerFinder}
+   * @return the UiElement matching {@code itemFinder}
+   * @throws ElementNotFoundException If no match is found
+   */
+  public UiElement scrollTo(Finder itemFinder) {
+    return scroller.scrollTo(driver, containerFinder, itemFinder);
+  }
+
+  /**
+   * Scrolls {@code containerFinder} in {@code direction} if necessary to find
+   * {@code itemFinder}, which is a descendant of {@code containerFinder}.
+   *
+   * @param itemFinder Finder for the desired item; relative to
+   *        {@code containerFinder}
+   * @param direction
+   * @return the UiElement matching {@code itemFinder}
+   * @throws ElementNotFoundException If no match is found
+   */
+  public UiElement scrollTo(Finder itemFinder, PhysicalDirection direction) {
+    return scroller.scrollTo(driver, containerFinder, itemFinder, direction);
+  }
+
+  /**
+   * Scrolls to {@code itemFinder} and returns true, otherwise returns false.
+   *
+   * @param itemFinder Finder for the desired item
+   * @return true if successful, otherwise false
+   */
+  public boolean canScrollTo(Finder itemFinder) {
+    try {
+      scrollTo(itemFinder);
+      return true;
+    } catch (ElementNotFoundException e) {
+      return false;
+    }
+  }
+}
diff --git a/src/com/google/android/droiddriver/instrumentation/InstrumentationContext.java b/src/com/google/android/droiddriver/instrumentation/InstrumentationContext.java
deleted file mode 100644
index 0c4e9dc..0000000
--- a/src/com/google/android/droiddriver/instrumentation/InstrumentationContext.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * 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.instrumentation;
-
-import android.app.Instrumentation;
-import android.view.InputEvent;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
-import android.view.View;
-
-import com.google.android.droiddriver.InputInjector;
-import com.google.android.droiddriver.base.AbstractContext;
-import com.google.android.droiddriver.exceptions.ActionException;
-import com.google.common.collect.MapMaker;
-
-import java.util.Map;
-
-/**
- * Internal helper for managing all instances.
- */
-public class InstrumentationContext extends AbstractContext {
-  private final Map<View, ViewElement> map = new MapMaker().weakKeys().weakValues().makeMap();
-
-  InstrumentationContext(final Instrumentation instrumentation) {
-    super(new InputInjector() {
-      @Override
-      public boolean injectInputEvent(InputEvent event) {
-        if (event instanceof MotionEvent) {
-          instrumentation.sendPointerSync((MotionEvent) event);
-        } else if (event instanceof KeyEvent) {
-          instrumentation.sendKeySync((KeyEvent) event);
-        } else {
-          throw new ActionException("Unknown input event type: " + event);
-        }
-        return true;
-      }
-    });
-  }
-
-  public ViewElement getUiElement(View view) {
-    ViewElement element = map.get(view);
-    if (element == null) {
-      element = new ViewElement(this, view);
-      map.put(view, element);
-    }
-    return element;
-  }
-
-  @Override
-  public void clearData() {
-    map.clear();
-  }
-}
diff --git a/src/com/google/android/droiddriver/instrumentation/InstrumentationDriver.java b/src/com/google/android/droiddriver/instrumentation/InstrumentationDriver.java
index c4f1fbb..8804211 100644
--- a/src/com/google/android/droiddriver/instrumentation/InstrumentationDriver.java
+++ b/src/com/google/android/droiddriver/instrumentation/InstrumentationDriver.java
@@ -16,90 +16,102 @@
 
 package com.google.android.droiddriver.instrumentation;
 
-import android.app.Activity;
 import android.app.Instrumentation;
-import android.graphics.Bitmap;
 import android.os.SystemClock;
+import android.util.Log;
 import android.view.View;
 
-import com.google.android.droiddriver.base.AbstractDroidDriver;
+import com.google.android.droiddriver.actions.InputInjector;
+import com.google.android.droiddriver.base.BaseDroidDriver;
+import com.google.android.droiddriver.base.DroidDriverContext;
+import com.google.android.droiddriver.exceptions.DroidDriverException;
 import com.google.android.droiddriver.exceptions.TimeoutException;
 import com.google.android.droiddriver.util.ActivityUtils;
-import com.google.common.primitives.Longs;
+import com.google.android.droiddriver.util.Logs;
 
 /**
- * Implementation of a UiDriver that is driven via instrumentation.
+ * Implementation of DroidDriver that is driven via instrumentation.
  */
-public class InstrumentationDriver extends AbstractDroidDriver {
-  private final InstrumentationContext context;
+public class InstrumentationDriver extends BaseDroidDriver<View, ViewElement> {
+  private final DroidDriverContext<View, ViewElement> context;
+  private final InputInjector injector;
+  private final InstrumentationUiDevice uiDevice;
 
   public InstrumentationDriver(Instrumentation instrumentation) {
-    super(instrumentation);
-    this.context = new InstrumentationContext(instrumentation);
+    context = new DroidDriverContext<View, ViewElement>(instrumentation, this);
+    injector = new InstrumentationInputInjector(instrumentation);
+    uiDevice = new InstrumentationUiDevice(context);
   }
 
   @Override
-  protected ViewElement getNewRootElement() {
-    return context.getUiElement(findRootView());
+  public InputInjector getInjector() {
+    return injector;
   }
 
   @Override
-  protected InstrumentationContext getContext() {
-    return context;
+  protected ViewElement newRootElement() {
+    return context.newRootElement(findRootView());
+  }
+
+  @Override
+  protected ViewElement newUiElement(View rawElement, ViewElement parent) {
+    return new ViewElement(context, rawElement, parent);
+  }
+
+  private static class FindRootViewRunnable implements Runnable {
+    View rootView;
+    Throwable exception;
+
+    @Override
+    public void run() {
+      try {
+        View[] views = RootFinder.getRootViews();
+        if (views.length > 1) {
+          Logs.log(Log.VERBOSE, "views.length=" + views.length);
+          for (View view : views) {
+            if (view.hasWindowFocus()) {
+              rootView = view;
+              return;
+            }
+          }
+        }
+        // Fall back to DecorView.
+        rootView = ActivityUtils.getRunningActivity().getWindow().getDecorView();
+      } catch (Throwable e) {
+        exception = e;
+        Logs.log(Log.ERROR, e);
+      }
+    }
   }
 
   private View findRootView() {
-    Activity runningActivity = getRunningActivity();
-    View[] views = RootFinder.getRootViews();
-    if (views.length > 1) {
-      for (View view : views) {
-        if (view.hasWindowFocus()) {
-          return view;
-        }
-      }
+    waitForRunningActivity();
+    FindRootViewRunnable findRootViewRunnable = new FindRootViewRunnable();
+    context.runOnMainSync(findRootViewRunnable);
+    if (findRootViewRunnable.exception != null) {
+      throw new DroidDriverException(findRootViewRunnable.exception);
     }
-    return runningActivity.getWindow().getDecorView();
+    return findRootViewRunnable.rootView;
   }
 
-  private Activity getRunningActivity() {
+  private void waitForRunningActivity() {
     long timeoutMillis = getPoller().getTimeoutMillis();
     long end = SystemClock.uptimeMillis() + timeoutMillis;
     while (true) {
-      instrumentation.waitForIdleSync();
-      Activity runningActivity = ActivityUtils.getRunningActivity();
-      if (runningActivity != null) {
-        return runningActivity;
+      if (ActivityUtils.getRunningActivity() != null) {
+        return;
       }
       long remainingMillis = end - SystemClock.uptimeMillis();
       if (remainingMillis < 0) {
         throw new TimeoutException(String.format(
             "Timed out after %d milliseconds waiting for foreground activity", timeoutMillis));
       }
-      SystemClock.sleep(Longs.min(250, remainingMillis));
-    }
-  }
-
-  private static class ScreenshotRunnable implements Runnable {
-    private final View rootView;
-    private Bitmap screenshot;
-
-    private ScreenshotRunnable(View rootView) {
-      this.rootView = rootView;
-    }
-
-    @Override
-    public void run() {
-      rootView.destroyDrawingCache();
-      rootView.buildDrawingCache(false);
-      screenshot = Bitmap.createBitmap(rootView.getDrawingCache());
-      rootView.destroyDrawingCache();
+      SystemClock.sleep(Math.min(250, remainingMillis));
     }
   }
 
   @Override
-  protected Bitmap takeScreenshot() {
-    ScreenshotRunnable screenshotRunnable = new ScreenshotRunnable(findRootView());
-    instrumentation.runOnMainSync(screenshotRunnable);
-    return screenshotRunnable.screenshot;
+  public InstrumentationUiDevice getUiDevice() {
+    return uiDevice;
   }
 }
diff --git a/src/com/google/android/droiddriver/instrumentation/InstrumentationInputInjector.java b/src/com/google/android/droiddriver/instrumentation/InstrumentationInputInjector.java
new file mode 100644
index 0000000..7e47e90
--- /dev/null
+++ b/src/com/google/android/droiddriver/instrumentation/InstrumentationInputInjector.java
@@ -0,0 +1,45 @@
+/*
+ * 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.instrumentation;
+
+import android.app.Instrumentation;
+import android.view.InputEvent;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+import com.google.android.droiddriver.actions.InputInjector;
+import com.google.android.droiddriver.exceptions.ActionException;
+
+public class InstrumentationInputInjector implements InputInjector {
+  private final Instrumentation instrumentation;
+
+  public InstrumentationInputInjector(Instrumentation instrumentation) {
+    this.instrumentation = instrumentation;
+  }
+
+  @Override
+  public boolean injectInputEvent(InputEvent event) {
+    if (event instanceof MotionEvent) {
+      instrumentation.sendPointerSync((MotionEvent) event);
+    } else if (event instanceof KeyEvent) {
+      instrumentation.sendKeySync((KeyEvent) event);
+    } else {
+      throw new ActionException("Unknown input event type: " + event);
+    }
+    return true;
+  }
+}
diff --git a/src/com/google/android/droiddriver/instrumentation/InstrumentationUiDevice.java b/src/com/google/android/droiddriver/instrumentation/InstrumentationUiDevice.java
new file mode 100644
index 0000000..f94137f
--- /dev/null
+++ b/src/com/google/android/droiddriver/instrumentation/InstrumentationUiDevice.java
@@ -0,0 +1,85 @@
+/*
+ * 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.instrumentation;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Bitmap.Config;
+import android.util.Log;
+import android.view.View;
+
+import com.google.android.droiddriver.base.BaseUiDevice;
+import com.google.android.droiddriver.base.DroidDriverContext;
+import com.google.android.droiddriver.util.Logs;
+
+class InstrumentationUiDevice extends BaseUiDevice {
+  private final DroidDriverContext<View, ViewElement> context;
+
+  InstrumentationUiDevice(DroidDriverContext<View, ViewElement> context) {
+    this.context = context;
+  }
+
+  @Override
+  protected Bitmap takeScreenshot() {
+    ScreenshotRunnable screenshotRunnable =
+        new ScreenshotRunnable(context.getDriver().getRootElement().getRawElement());
+    context.runOnMainSync(screenshotRunnable);
+    return screenshotRunnable.screenshot;
+  }
+
+  @Override
+  protected DroidDriverContext<View, ViewElement> getContext() {
+    return context;
+  }
+
+  private static class ScreenshotRunnable implements Runnable {
+    private final View rootView;
+    Bitmap screenshot;
+
+    private ScreenshotRunnable(View rootView) {
+      this.rootView = rootView;
+    }
+
+    @Override
+    public void run() {
+      try {
+        rootView.destroyDrawingCache();
+        rootView.buildDrawingCache(false);
+        Bitmap drawingCache = rootView.getDrawingCache();
+        int[] xy = new int[2];
+        rootView.getLocationOnScreen(xy);
+        if (xy[0] == 0 && xy[1] == 0) {
+          screenshot = Bitmap.createBitmap(drawingCache);
+        } else {
+          Canvas canvas = new Canvas();
+          Rect rect = new Rect(0, 0, drawingCache.getWidth(), drawingCache.getHeight());
+          rect.offset(xy[0], xy[1]);
+          screenshot =
+              Bitmap.createBitmap(rect.width() + xy[0], rect.height() + xy[1], Config.ARGB_8888);
+          canvas.setBitmap(screenshot);
+          canvas.drawBitmap(drawingCache, null, new RectF(rect), null);
+          canvas.setBitmap(null);
+        }
+        rootView.destroyDrawingCache();
+      } catch (Throwable e) {
+        Logs.log(Log.ERROR, e);
+      }
+    }
+  }
+}
diff --git a/src/com/google/android/droiddriver/instrumentation/RootFinder.java b/src/com/google/android/droiddriver/instrumentation/RootFinder.java
index 19b8387..eec06c0 100644
--- a/src/com/google/android/droiddriver/instrumentation/RootFinder.java
+++ b/src/com/google/android/droiddriver/instrumentation/RootFinder.java
@@ -16,20 +16,17 @@
 
 package com.google.android.droiddriver.instrumentation;
 
-import com.google.android.droiddriver.exceptions.DroidDriverException;
-import com.google.android.droiddriver.util.Logs;
-
 import android.os.Build;
-import android.util.Log;
 import android.view.View;
 
+import com.google.android.droiddriver.exceptions.DroidDriverException;
+
 import java.lang.reflect.Field;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 
 /**
  * Class to find the root view.
- * Note(twickham): This class is no longer being used.
  */
 public class RootFinder {
 
@@ -38,8 +35,9 @@
   private static final Object windowManagerObj;
 
   static {
-    String windowManagerClassName = Build.VERSION.SDK_INT >= 17 ? "android.view.WindowManagerGlobal"
-        : "android.view.WindowManagerImpl";
+    String windowManagerClassName =
+        Build.VERSION.SDK_INT >= 17 ? "android.view.WindowManagerGlobal"
+            : "android.view.WindowManagerImpl";
     String instanceMethod = Build.VERSION.SDK_INT >= 17 ? "getInstance" : "getDefault";
     try {
       Class<?> clazz = Class.forName(windowManagerClassName);
@@ -51,8 +49,8 @@
       throw new DroidDriverException(String.format("could not invoke: %s on %s", instanceMethod,
           windowManagerClassName), ite.getCause());
     } catch (ClassNotFoundException cnfe) {
-      throw new DroidDriverException(
-          String.format("could not find class: %s", windowManagerClassName), cnfe);
+      throw new DroidDriverException(String.format("could not find class: %s",
+          windowManagerClassName), cnfe);
     } catch (NoSuchFieldException nsfe) {
       throw new DroidDriverException(String.format("could not find field: %s on %s",
           VIEW_FIELD_NAME, windowManagerClassName), nsfe);
@@ -60,13 +58,13 @@
       throw new DroidDriverException(String.format("could not find method: %s on %s",
           instanceMethod, windowManagerClassName), nsme);
     } catch (RuntimeException re) {
-      throw new DroidDriverException(
-          String.format("reflective setup failed using obj: %s method: %s field: %s",
-          windowManagerClassName, instanceMethod, VIEW_FIELD_NAME), re);
+      throw new DroidDriverException(String.format(
+          "reflective setup failed using obj: %s method: %s field: %s", windowManagerClassName,
+          instanceMethod, VIEW_FIELD_NAME), re);
     } catch (IllegalAccessException iae) {
-      throw new DroidDriverException(
-          String.format("reflective setup failed using obj: %s method: %s field: %s",
-          windowManagerClassName, instanceMethod, VIEW_FIELD_NAME), iae);
+      throw new DroidDriverException(String.format(
+          "reflective setup failed using obj: %s method: %s field: %s", windowManagerClassName,
+          instanceMethod, VIEW_FIELD_NAME), iae);
     }
   }
 
@@ -78,12 +76,10 @@
 
     try {
       views = (View[]) viewsField.get(windowManagerObj);
-      Logs.log(Log.DEBUG, "View size:" +views.length);
       return views;
     } catch (RuntimeException re) {
       throw new DroidDriverException(String.format("Reflective access to %s on %s failed.",
           viewsField, windowManagerObj), re);
-
     } catch (IllegalAccessException iae) {
       throw new DroidDriverException(String.format("Reflective access to %s on %s failed.",
           viewsField, windowManagerObj), iae);
diff --git a/src/com/google/android/droiddriver/instrumentation/ViewElement.java b/src/com/google/android/droiddriver/instrumentation/ViewElement.java
index 613fa09..d1379cd 100644
--- a/src/com/google/android/droiddriver/instrumentation/ViewElement.java
+++ b/src/com/google/android/droiddriver/instrumentation/ViewElement.java
@@ -16,36 +16,172 @@
 
 package com.google.android.droiddriver.instrumentation;
 
-import static com.google.android.droiddriver.util.TextUtils.charSequenceToString;
+import static com.google.android.droiddriver.util.Strings.charSequenceToString;
 
 import android.content.res.Resources;
 import android.graphics.Rect;
-import android.util.Log;
 import android.view.View;
 import android.view.ViewGroup;
-import android.view.ViewParent;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.Checkable;
 import android.widget.TextView;
 
-import com.google.android.droiddriver.InputInjector;
-import com.google.android.droiddriver.base.AbstractUiElement;
-import com.google.android.droiddriver.util.Logs;
-import com.google.common.base.Preconditions;
-import com.google.common.collect.Maps;
+import com.google.android.droiddriver.actions.InputInjector;
+import com.google.android.droiddriver.base.BaseUiElement;
+import com.google.android.droiddriver.base.DroidDriverContext;
+import com.google.android.droiddriver.exceptions.DroidDriverException;
+import com.google.android.droiddriver.finders.Attribute;
+import com.google.android.droiddriver.util.Preconditions;
 
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
+import java.util.concurrent.FutureTask;
 
 /**
  * A UiElement that is backed by a View.
  */
-// TODO: always accessing view on the UI thread even when only get access is
-// needed -- the field may be in the middle of updating.
-public class ViewElement extends AbstractUiElement {
-  private static final Map<String, String> CLASS_NAME_OVERRIDES = Maps.newHashMap();
+public class ViewElement extends BaseUiElement<View, ViewElement> {
+  private static class SnapshotViewAttributesRunnable implements Runnable {
+    private final View view;
+    final Map<Attribute, Object> attribs = new EnumMap<Attribute, Object>(Attribute.class);
+    boolean visible;
+    Rect visibleBounds;
+    List<View> childViews;
+    Throwable exception;
 
-  private final InstrumentationContext context;
-  private final View view;
+    private SnapshotViewAttributesRunnable(View view) {
+      this.view = view;
+    }
+
+    @Override
+    public void run() {
+      try {
+        put(Attribute.PACKAGE, view.getContext().getPackageName());
+        put(Attribute.CLASS, getClassName());
+        put(Attribute.TEXT, getText());
+        put(Attribute.CONTENT_DESC, charSequenceToString(view.getContentDescription()));
+        put(Attribute.RESOURCE_ID, getResourceId());
+        put(Attribute.CHECKABLE, view instanceof Checkable);
+        put(Attribute.CHECKED, isChecked());
+        put(Attribute.CLICKABLE, view.isClickable());
+        put(Attribute.ENABLED, view.isEnabled());
+        put(Attribute.FOCUSABLE, view.isFocusable());
+        put(Attribute.FOCUSED, view.isFocused());
+        put(Attribute.LONG_CLICKABLE, view.isLongClickable());
+        put(Attribute.PASSWORD, isPassword());
+        put(Attribute.SCROLLABLE, isScrollable());
+        if (view instanceof TextView) {
+          TextView textView = (TextView) view;
+          if (textView.hasSelection()) {
+            attribs.put(Attribute.SELECTION_START, textView.getSelectionStart());
+            attribs.put(Attribute.SELECTION_END, textView.getSelectionEnd());
+          }
+        }
+        put(Attribute.SELECTED, view.isSelected());
+        put(Attribute.BOUNDS, getBounds());
+
+        // Order matters as setVisible() depends on setVisibleBounds().
+        this.visibleBounds = getVisibleBounds();
+        // isShown() checks the visibility flag of this view and ancestors; it
+        // needs to have the VISIBLE flag as well as non-empty bounds to be
+        // visible.
+        this.visible = view.isShown() && !visibleBounds.isEmpty();
+        setChildViews();
+      } catch (Throwable e) {
+        exception = e;
+      }
+    }
+
+    private void put(Attribute key, Object value) {
+      if (value != null) {
+        attribs.put(key, value);
+      }
+    }
+
+    private String getText() {
+      if (!(view instanceof TextView)) {
+        return null;
+      }
+      return charSequenceToString(((TextView) view).getText());
+    }
+
+    private String getClassName() {
+      String className = view.getClass().getName();
+      return CLASS_NAME_OVERRIDES.containsKey(className) ? CLASS_NAME_OVERRIDES.get(className)
+          : className;
+    }
+
+    private String getResourceId() {
+      if (view.getId() != View.NO_ID && view.getResources() != null) {
+        try {
+          return charSequenceToString(view.getResources().getResourceName(view.getId()));
+        } catch (Resources.NotFoundException nfe) {
+          /* ignore */
+        }
+      }
+      return null;
+    }
+
+    private boolean isChecked() {
+      return view instanceof Checkable && ((Checkable) view).isChecked();
+    }
+
+    private boolean isScrollable() {
+      // TODO: find a meaningful implementation
+      return true;
+    }
+
+    private boolean isPassword() {
+      // TODO: find a meaningful implementation
+      return false;
+    }
+
+    private Rect getBounds() {
+      Rect rect = new Rect();
+      int[] xy = new int[2];
+      view.getLocationOnScreen(xy);
+      rect.set(xy[0], xy[1], xy[0] + view.getWidth(), xy[1] + view.getHeight());
+      return rect;
+    }
+
+    private Rect getVisibleBounds() {
+      Rect visibleBounds = new Rect();
+      if (!view.isShown() || !view.getGlobalVisibleRect(visibleBounds)) {
+        visibleBounds.setEmpty();
+      }
+      int[] xyScreen = new int[2];
+      view.getLocationOnScreen(xyScreen);
+      int[] xyWindow = new int[2];
+      view.getLocationInWindow(xyWindow);
+      int windowLeft = xyScreen[0] - xyWindow[0];
+      int windowTop = xyScreen[1] - xyWindow[1];
+
+      // Bounds are relative to root view; adjust to screen coordinates.
+      visibleBounds.offset(windowLeft, windowTop);
+      return visibleBounds;
+    }
+
+    private void setChildViews() {
+      if (!(view instanceof ViewGroup)) {
+        return;
+      }
+      ViewGroup group = (ViewGroup) view;
+      int childCount = group.getChildCount();
+      childViews = new ArrayList<View>(childCount);
+      for (int i = 0; i < childCount; i++) {
+        View child = group.getChildAt(i);
+        if (child != null) {
+          childViews.add(child);
+        }
+      }
+    }
+  }
+
+  private static final Map<String, String> CLASS_NAME_OVERRIDES = new HashMap<String, String>();
 
   /**
    * Typically users find the class name to use in tests using SDK tool
@@ -61,166 +197,90 @@
    * {@link com.google.android.droiddriver.DroidDriver#dumpUiElementTree}, then
    * call this method in setUp to override it with the class name seen in
    * uiautomatorviewer.
+   * </p>
+   * A better solution is to use resource-id instead of classname, which is an
+   * implementation detail and subject to change.
    */
   public static void overrideClassName(String actualClassName, String overridingClassName) {
     CLASS_NAME_OVERRIDES.put(actualClassName, overridingClassName);
   }
 
-  public ViewElement(InstrumentationContext context, View view) {
+  private final DroidDriverContext<View, ViewElement> context;
+  private final View view;
+  private final Map<Attribute, Object> attributes;
+  private final boolean visible;
+  private final Rect visibleBounds;
+  private final ViewElement parent;
+  private final List<ViewElement> children;
+
+  /**
+   * A snapshot of all attributes is taken at construction. The attributes of a
+   * {@code ViewElement} instance are immutable. If the underlying view is
+   * updated, a new {@code ViewElement} instance will be created in
+   * {@link com.google.android.droiddriver.DroidDriver#refreshUiElementTree}.
+   */
+  public ViewElement(DroidDriverContext<View, ViewElement> context, View view, ViewElement parent) {
     this.context = Preconditions.checkNotNull(context);
     this.view = Preconditions.checkNotNull(view);
-  }
-
-  @Override
-  public String getText() {
-    if (!(view instanceof TextView)) {
-      return null;
+    this.parent = parent;
+    SnapshotViewAttributesRunnable attributesSnapshot = new SnapshotViewAttributesRunnable(view);
+    context.runOnMainSync(attributesSnapshot);
+    if (attributesSnapshot.exception != null) {
+      throw new DroidDriverException(attributesSnapshot.exception);
     }
-    return charSequenceToString(((TextView) view).getText());
-  }
 
-  @Override
-  public String getContentDescription() {
-    return charSequenceToString(view.getContentDescription());
-  }
-
-  @Override
-  public String getClassName() {
-    String className = view.getClass().getName();
-    return CLASS_NAME_OVERRIDES.containsKey(className) ? CLASS_NAME_OVERRIDES.get(className)
-        : className;
-  }
-
-  @Override
-  public String getResourceId() {
-    if (view.getId() != View.NO_ID && view.getResources() != null) {
-      try {
-        return charSequenceToString(view.getResources().getResourceName(view.getId()));
-      } catch (Resources.NotFoundException nfe) {
-        /* ignore */
+    attributes = Collections.unmodifiableMap(attributesSnapshot.attribs);
+    this.visibleBounds = attributesSnapshot.visibleBounds;
+    this.visible = attributesSnapshot.visible;
+    if (attributesSnapshot.childViews == null) {
+      this.children = null;
+    } else {
+      List<ViewElement> children = new ArrayList<ViewElement>(attributesSnapshot.childViews.size());
+      for (View childView : attributesSnapshot.childViews) {
+        children.add(context.getElement(childView, this));
       }
+      this.children = Collections.unmodifiableList(children);
     }
-    return null;
-  }
-
-  @Override
-  public String getPackageName() {
-    return view.getContext().getPackageName();
-  }
-
-  @Override
-  public InputInjector getInjector() {
-    return context.getInjector();
-  }
-
-  @Override
-  public boolean isVisible() {
-    // isShown() checks the visibility flag of this view and ancestors; it needs
-    // to have the VISIBLE flag as well as non-empty bounds to be visible.
-    return view.isShown() && !getVisibleBounds().isEmpty();
-  }
-
-  @Override
-  public boolean isCheckable() {
-    return view instanceof Checkable;
-  }
-
-  @Override
-  public boolean isChecked() {
-    if (!isCheckable()) {
-      return false;
-    }
-    return ((Checkable) view).isChecked();
-  }
-
-  @Override
-  public boolean isClickable() {
-    return view.isClickable();
-  }
-
-  @Override
-  public boolean isEnabled() {
-    return view.isEnabled();
-  }
-
-  @Override
-  public boolean isFocusable() {
-    return view.isFocusable();
-  }
-
-  @Override
-  public boolean isFocused() {
-    return view.isFocused();
-  }
-
-  @Override
-  public boolean isScrollable() {
-    // TODO: find a meaningful implementation
-    return true;
-  }
-
-  @Override
-  public boolean isLongClickable() {
-    return view.isLongClickable();
-  }
-
-  @Override
-  public boolean isPassword() {
-    // TODO: find a meaningful implementation
-    return false;
-  }
-
-  @Override
-  public boolean isSelected() {
-    return view.isSelected();
-  }
-
-  @Override
-  public Rect getBounds() {
-    Rect rect = new Rect();
-    int[] xy = new int[2];
-    view.getLocationOnScreen(xy);
-    rect.set(xy[0], xy[1], xy[0] + view.getWidth(), xy[1] + view.getHeight());
-    return rect;
   }
 
   @Override
   public Rect getVisibleBounds() {
-    Rect visibleBounds = new Rect();
-    if (!view.getGlobalVisibleRect(visibleBounds)) {
-      Logs.log(Log.VERBOSE, "View is invisible: " + toString());
-      visibleBounds.setEmpty();
-    }
-    int[] xy = new int[2];
-    view.getLocationOnScreen(xy);
-    // Bounds are relative to root view; adjust to screen coordinates.
-    visibleBounds.offsetTo(xy[0], xy[1]);
     return visibleBounds;
   }
 
   @Override
-  public int getChildCount() {
-    if (!(view instanceof ViewGroup)) {
-      return 0;
-    }
-    return ((ViewGroup) view).getChildCount();
-  }
-
-  @Override
-  public ViewElement getChild(int index) {
-    if (!(view instanceof ViewGroup)) {
-      return null;
-    }
-    View child = ((ViewGroup) view).getChildAt(index);
-    return child == null ? null : context.getUiElement(child);
+  public boolean isVisible() {
+    return visible;
   }
 
   @Override
   public ViewElement getParent() {
-    ViewParent parent = view.getParent();
-    if (!(parent instanceof View)) {
-      return null;
-    }
-    return context.getUiElement((View) parent);
+    return parent;
+  }
+
+  @Override
+  protected List<ViewElement> getChildren() {
+    return children;
+  }
+
+  @Override
+  protected Map<Attribute, Object> getAttributes() {
+    return attributes;
+  }
+
+  @Override
+  public InputInjector getInjector() {
+    return context.getDriver().getInjector();
+  }
+
+  @Override
+  protected void doPerformAndWait(FutureTask<Boolean> futureTask, long timeoutMillis) {
+    futureTask.run();
+    context.tryWaitForIdleSync(timeoutMillis);
+  }
+
+  @Override
+  public View getRawElement() {
+    return view;
   }
 }
diff --git a/src/com/google/android/droiddriver/runner/TestRunner.java b/src/com/google/android/droiddriver/runner/TestRunner.java
index 4474894..e6277fb 100644
--- a/src/com/google/android/droiddriver/runner/TestRunner.java
+++ b/src/com/google/android/droiddriver/runner/TestRunner.java
@@ -25,28 +25,30 @@
 import android.util.Log;
 
 import com.android.internal.util.Predicate;
+import com.google.android.droiddriver.helpers.DroidDrivers;
 import com.google.android.droiddriver.util.ActivityUtils;
+import com.google.android.droiddriver.util.ActivityUtils.Supplier;
 import com.google.android.droiddriver.util.Logs;
-import com.google.common.base.Supplier;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Sets;
 
 import junit.framework.AssertionFailedError;
 import junit.framework.Test;
 import junit.framework.TestListener;
 
 import java.lang.annotation.Annotation;
-import java.util.Iterator;
+import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeUnit;
 
 /**
  * Adds activity watcher to InstrumentationTestRunner.
  */
 public class TestRunner extends InstrumentationTestRunner {
-  private final Set<Activity> activities = Sets.newIdentityHashSet();
+  private final Set<Activity> activities = new HashSet<Activity>();
   private final AndroidTestRunner androidTestRunner = new AndroidTestRunner();
-  private Activity runningActivity;
+  private volatile Activity runningActivity;
 
   /**
    * Returns an {@link AndroidTestRunner} that is shared by this and super, such
@@ -64,27 +66,42 @@
    */
   @Override
   public void onStart() {
+    DroidDrivers.initInstrumentation(this, getArguments());
+
     getAndroidTestRunner().addTestListener(new TestListener() {
       @Override
       public void endTest(Test test) {
-        runOnMainSync(new Runnable() {
+        // Try to finish activity on best-effort basis - TestListener should
+        // not throw.
+        final Activity[] activitiesCopy;
+        synchronized (activities) {
+          if (activities.isEmpty()) {
+            return;
+          }
+          activitiesCopy = activities.toArray(new Activity[activities.size()]);
+        }
+
+        runOnMainSyncWithTimeLimit(new Runnable() {
           @Override
           public void run() {
-            Iterator<Activity> iterator = activities.iterator();
-            while (iterator.hasNext()) {
-              Activity activity = iterator.next();
-              iterator.remove();
+            for (Activity activity : activitiesCopy) {
               if (!activity.isFinishing()) {
                 try {
                   Logs.log(Log.INFO, "Stopping activity: " + activity);
                   activity.finish();
-                } catch (RuntimeException e) {
+                } catch (Throwable e) {
                   Logs.log(Log.ERROR, e, "Failed to stop activity");
                 }
               }
             }
           }
         });
+
+        // We've done what we can. Clear activities if any are left.
+        synchronized (activities) {
+          activities.clear();
+          runningActivity = null;
+        }
       }
 
       @Override
@@ -109,7 +126,7 @@
 
   // Overrides InstrumentationTestRunner
   List<Predicate<TestMethod>> getBuilderRequirements() {
-    List<Predicate<TestMethod>> requirements = Lists.newArrayList();
+    List<Predicate<TestMethod>> requirements = new ArrayList<Predicate<TestMethod>>();
     requirements.add(new Predicate<TestMethod>() {
       @Override
       public boolean apply(TestMethod arg0) {
@@ -121,7 +138,7 @@
         }
 
         UseUiAutomation useUiAutomation = getAnnotation(arg0, UseUiAutomation.class);
-        if (useUiAutomation != null && Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
+        if (useUiAutomation != null && !DroidDrivers.hasUiAutomation()) {
           Logs.logfmt(Log.INFO,
               "filtered %s#%s: Has @UseUiAutomation, but ro.build.version.sdk=%d",
               arg0.getEnclosingClassname(), arg0.getName(), Build.VERSION.SDK_INT);
@@ -144,13 +161,17 @@
   @Override
   public void callActivityOnDestroy(Activity activity) {
     super.callActivityOnDestroy(activity);
-    activities.remove(activity);
+    synchronized (activities) {
+      activities.remove(activity);
+    }
   }
 
   @Override
   public void callActivityOnCreate(Activity activity, Bundle bundle) {
     super.callActivityOnCreate(activity, bundle);
-    activities.add(activity);
+    synchronized (activities) {
+      activities.add(activity);
+    }
   }
 
   @Override
@@ -162,8 +183,31 @@
   @Override
   public void callActivityOnPause(Activity activity) {
     super.callActivityOnPause(activity);
-    if (activity == ActivityUtils.getRunningActivity()) {
+    if (activity == runningActivity) {
       runningActivity = null;
     }
   }
+
+  private boolean runOnMainSyncWithTimeLimit(Runnable runnable) {
+    // Do we need it configurable? Now only used in endTest.
+    long timeoutMillis = 10000L;
+    final FutureTask<?> futureTask = new FutureTask<Void>(runnable, null);
+    new Thread(new Runnable() {
+      @Override
+      public void run() {
+        runOnMainSync(futureTask);
+      }
+    }).start();
+
+    try {
+      futureTask.get(timeoutMillis, TimeUnit.MILLISECONDS);
+      return true;
+    } catch (Throwable e) {
+      Logs.log(Log.WARN, e, String.format(
+          "Timed out after %d milliseconds waiting for Instrumentation.runOnMainSync",
+          timeoutMillis));
+      futureTask.cancel(false);
+      return false;
+    }
+  }
 }
diff --git a/src/com/google/android/droiddriver/scroll/AbstractSentinelStrategy.java b/src/com/google/android/droiddriver/scroll/AbstractSentinelStrategy.java
deleted file mode 100644
index 2d77beb..0000000
--- a/src/com/google/android/droiddriver/scroll/AbstractSentinelStrategy.java
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * 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.scroll;
-
-import com.google.android.droiddriver.UiElement;
-import com.google.android.droiddriver.actions.ScrollDirection;
-import com.google.android.droiddriver.scroll.Direction.LogicalDirection;
-import com.google.android.droiddriver.scroll.Direction.PhysicalToLogicalConverter;
-import com.google.common.base.Predicate;
-import com.google.common.base.Predicates;
-
-import java.util.List;
-
-/**
- * Base {@link SentinelStrategy} for common code.
- */
-public abstract class AbstractSentinelStrategy implements SentinelStrategy {
-
-  /**
-   * Gets sentinel based on {@link Predicate}.
-   */
-  public static abstract class GetStrategy {
-    protected final Predicate<? super UiElement> predicate;
-    protected final String description;
-
-    protected GetStrategy(Predicate<? super UiElement> predicate, String description) {
-      this.predicate = predicate;
-      this.description = description;
-    }
-
-    public UiElement getSentinel(UiElement parent) {
-      return getSentinel(parent.getChildren(predicate));
-    }
-
-    protected abstract UiElement getSentinel(List<UiElement> children);
-
-    @Override
-    public String toString() {
-      return description;
-    }
-  }
-
-  /**
-   * Decorates an existing {@link GetStrategy} by adding another
-   * {@link Predicate}.
-   */
-  public static class MorePredicateGetStrategy extends GetStrategy {
-    private final GetStrategy original;
-
-    public MorePredicateGetStrategy(GetStrategy original,
-        Predicate<? super UiElement> extraPredicate, String extraDescription) {
-      super(Predicates.and(original.predicate, extraPredicate), extraDescription
-          + original.description);
-      this.original = original;
-    }
-
-    @Override
-    protected UiElement getSentinel(List<UiElement> children) {
-      return original.getSentinel(children);
-    }
-  }
-
-  /**
-   * Returns the first child as the sentinel.
-   */
-  public static final GetStrategy FIRST_CHILD_GETTER = new GetStrategy(Predicates.alwaysTrue(),
-      "FIRST_CHILD") {
-    @Override
-    protected UiElement getSentinel(List<UiElement> children) {
-      return children.get(0);
-    }
-  };
-
-  /**
-   * Returns the last child as the sentinel.
-   */
-  public static final GetStrategy LAST_CHILD_GETTER = new GetStrategy(Predicates.alwaysTrue(),
-      "LAST_CHILD") {
-    @Override
-    protected UiElement getSentinel(List<UiElement> children) {
-      return children.get(children.size() - 1);
-    }
-  };
-
-  /**
-   * Returns the second child as the sentinel. Useful when the activity shows a
-   * fixed first child.
-   */
-  public static final GetStrategy SECOND_CHILD_GETTER = new GetStrategy(Predicates.alwaysTrue(),
-      "SECOND_CHILD") {
-    @Override
-    protected UiElement getSentinel(List<UiElement> children) {
-      return children.get(1);
-    }
-  };
-
-  protected final GetStrategy backwardGetStrategy;
-  protected final GetStrategy forwardGetStrategy;
-  protected final PhysicalToLogicalConverter physicalToLogicalConverter;
-
-  public AbstractSentinelStrategy(GetStrategy backwardGetStrategy, GetStrategy forwardGetStrategy,
-      PhysicalToLogicalConverter physicalToLogicalConverter) {
-    this.backwardGetStrategy = backwardGetStrategy;
-    this.forwardGetStrategy = forwardGetStrategy;
-    this.physicalToLogicalConverter = physicalToLogicalConverter;
-  }
-
-  protected UiElement getSentinel(UiElement parent, ScrollDirection direction) {
-    LogicalDirection logicalDirection = physicalToLogicalConverter.toLogicalDirection(direction);
-    if (logicalDirection == LogicalDirection.BACKWARD) {
-      return backwardGetStrategy.getSentinel(parent);
-    } else {
-      return forwardGetStrategy.getSentinel(parent);
-    }
-  }
-
-  @Override
-  public String toString() {
-    return String.format("{backwardGetStrategy=%s, forwardGetStrategy=%s}", backwardGetStrategy,
-        forwardGetStrategy);
-  }
-}
diff --git a/src/com/google/android/droiddriver/scroll/AccessibilityEventScrollStepStrategy.java b/src/com/google/android/droiddriver/scroll/AccessibilityEventScrollStepStrategy.java
new file mode 100644
index 0000000..f903746
--- /dev/null
+++ b/src/com/google/android/droiddriver/scroll/AccessibilityEventScrollStepStrategy.java
@@ -0,0 +1,218 @@
+/*
+ * 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.scroll;
+
+import android.app.UiAutomation;
+import android.app.UiAutomation.AccessibilityEventFilter;
+import android.util.Log;
+import android.view.accessibility.AccessibilityEvent;
+
+import com.google.android.droiddriver.DroidDriver;
+import com.google.android.droiddriver.UiElement;
+import com.google.android.droiddriver.actions.SwipeAction;
+import com.google.android.droiddriver.exceptions.UnrecoverableException;
+import com.google.android.droiddriver.finders.Finder;
+import com.google.android.droiddriver.scroll.Direction.Axis;
+import com.google.android.droiddriver.scroll.Direction.DirectionConverter;
+import com.google.android.droiddriver.scroll.Direction.PhysicalDirection;
+import com.google.android.droiddriver.util.Logs;
+
+import java.util.concurrent.TimeoutException;
+
+/**
+ * A {@link ScrollStepStrategy} that determines whether more scrolling is
+ * possible by checking the {@link AccessibilityEvent} returned by
+ * {@link android.app.UiAutomation}.
+ * <p>
+ * This implementation behaves just like the <a href=
+ * "http://developer.android.com/tools/help/uiautomator/UiScrollable.html"
+ * >UiScrollable</a> class. It may not work in all cases. For instance,
+ * sometimes {@link android.support.v4.widget.DrawerLayout} does not send
+ * correct {@link AccessibilityEvent}s after scrolling.
+ * </p>
+ */
+public class AccessibilityEventScrollStepStrategy implements ScrollStepStrategy {
+  /**
+   * Stores the data if we reached end at the last {@link #scroll}. If the data
+   * match when a new scroll is requested, we can return immediately.
+   */
+  private static class EndData {
+    private Finder containerFinderAtEnd;
+    private PhysicalDirection directionAtEnd;
+
+    public boolean match(Finder containerFinder, PhysicalDirection direction) {
+      return containerFinderAtEnd == containerFinder && directionAtEnd == direction;
+    }
+
+    public void set(Finder containerFinder, PhysicalDirection direction) {
+      containerFinderAtEnd = containerFinder;
+      directionAtEnd = direction;
+    }
+
+    public void reset() {
+      set(null, null);
+    }
+  }
+
+  /**
+   * This filter allows us to grab the last accessibility event generated for a
+   * scroll up to {@code scrollEventTimeoutMillis}.
+   */
+  private static class LastScrollEventFilter implements AccessibilityEventFilter {
+    private AccessibilityEvent lastEvent;
+
+    @Override
+    public boolean accept(AccessibilityEvent event) {
+      if ((event.getEventType() & AccessibilityEvent.TYPE_VIEW_SCROLLED) != 0) {
+        // Recycle the current last event.
+        if (lastEvent != null) {
+          lastEvent.recycle();
+        }
+        lastEvent = AccessibilityEvent.obtain(event);
+      }
+      // Return false to collect events until scrollEventTimeoutMillis has
+      // elapsed.
+      return false;
+    }
+
+    public AccessibilityEvent getLastEvent() {
+      return lastEvent;
+    }
+  }
+
+  private final UiAutomation uiAutomation;
+  private final long scrollEventTimeoutMillis;
+  private final DirectionConverter directionConverter;
+  private final EndData endData = new EndData();
+
+  public AccessibilityEventScrollStepStrategy(UiAutomation uiAutomation,
+      long scrollEventTimeoutMillis, DirectionConverter converter) {
+    this.uiAutomation = uiAutomation;
+    this.scrollEventTimeoutMillis = scrollEventTimeoutMillis;
+    this.directionConverter = converter;
+  }
+
+  @Override
+  public boolean scroll(DroidDriver driver, Finder containerFinder,
+      final PhysicalDirection direction) {
+    // Check if we've reached end after last scroll.
+    if (endData.match(containerFinder, direction)) {
+      return false;
+    }
+
+    AccessibilityEvent event = doScrollAndReturnEvent(driver.on(containerFinder), direction);
+    if (detectEnd(event, direction.axis())) {
+      endData.set(containerFinder, direction);
+      Logs.log(Log.DEBUG, "reached scroll end with event: " + event);
+    }
+
+    // Clean up the event after use.
+    if (event != null) {
+      event.recycle();
+    }
+
+    // Even if event == null, that does not mean scroll has no effect!
+    // Some views may not emit correct events when the content changed.
+    return true;
+  }
+
+  // Copied from UiAutomator.
+  // AdapterViews have indices we can use to check for the beginning.
+  protected boolean detectEnd(AccessibilityEvent event, Axis axis) {
+    if (event == null) {
+      return true;
+    }
+
+    if (event.getFromIndex() != -1 && event.getToIndex() != -1 && event.getItemCount() != -1) {
+      return event.getFromIndex() == 0 || (event.getItemCount() - 1) == event.getToIndex();
+    }
+    if (event.getScrollX() != -1 && event.getScrollY() != -1) {
+      if (axis == Axis.VERTICAL) {
+        return event.getScrollY() == 0 || event.getScrollY() == event.getMaxScrollY();
+      } else if (axis == Axis.HORIZONTAL) {
+        return event.getScrollX() == 0 || event.getScrollX() == event.getMaxScrollX();
+      }
+    }
+
+    // This case is different from UiAutomator.
+    return event.getFromIndex() == -1 && event.getToIndex() == -1 && event.getItemCount() == -1
+        && event.getScrollX() == -1 && event.getScrollY() == -1;
+  }
+
+  @Override
+  public final DirectionConverter getDirectionConverter() {
+    return directionConverter;
+  }
+
+  @Override
+  public void beginScrolling(DroidDriver driver, Finder containerFinder, Finder itemFinder,
+      PhysicalDirection direction) {
+    endData.reset();
+  }
+
+  @Override
+  public void endScrolling(DroidDriver driver, Finder containerFinder, Finder itemFinder,
+      PhysicalDirection direction) {}
+
+  protected AccessibilityEvent doScrollAndReturnEvent(final UiElement container,
+      final PhysicalDirection direction) {
+    LastScrollEventFilter filter = new LastScrollEventFilter();
+    try {
+      uiAutomation.executeAndWaitForEvent(new Runnable() {
+        @Override
+        public void run() {
+          doScroll(container, direction);
+        }
+      }, filter, scrollEventTimeoutMillis);
+    } catch (IllegalStateException e) {
+      throw new UnrecoverableException(e);
+    } catch (TimeoutException e) {
+      // We expect this because LastScrollEventFilter.accept always returns
+      // false.
+    }
+    return filter.getLastEvent();
+  }
+
+  @Override
+  public void doScroll(final UiElement container, final PhysicalDirection direction) {
+    // 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.
+    container
+        .perform(new SwipeAction(direction, SwipeAction.getScrollSteps(), false /* drag */, 0L/* timeoutMillis */));
+  }
+
+  /**
+   * Some widgets may not always fire correct {@link AccessibilityEvent}.
+   * Detecting end by null event is safer (at the cost of a extra scroll) than
+   * examining indices.
+   */
+  public static class NullAccessibilityEventScrollStepStrategy extends
+      AccessibilityEventScrollStepStrategy {
+
+    public NullAccessibilityEventScrollStepStrategy(UiAutomation uiAutomation,
+        long scrollEventTimeoutMillis, DirectionConverter converter) {
+      super(uiAutomation, scrollEventTimeoutMillis, converter);
+    }
+
+    @Override
+    protected boolean detectEnd(AccessibilityEvent event, Axis axis) {
+      return event == null;
+    }
+  }
+}
diff --git a/src/com/google/android/droiddriver/scroll/Direction.java b/src/com/google/android/droiddriver/scroll/Direction.java
index 1f8f86b..cbbfd6b 100644
--- a/src/com/google/android/droiddriver/scroll/Direction.java
+++ b/src/com/google/android/droiddriver/scroll/Direction.java
@@ -15,64 +15,179 @@
  */
 package com.google.android.droiddriver.scroll;
 
-import com.google.android.droiddriver.actions.ScrollDirection;
-import com.google.android.droiddriver.exceptions.ActionException;
+import static com.google.android.droiddriver.scroll.Direction.PhysicalDirection.DOWN;
+import static com.google.android.droiddriver.scroll.Direction.PhysicalDirection.LEFT;
+import static com.google.android.droiddriver.scroll.Direction.PhysicalDirection.RIGHT;
+import static com.google.android.droiddriver.scroll.Direction.PhysicalDirection.UP;
 
 /**
- * Interfaces and constants for scroll directions.
+ * A namespace to hold interfaces and constants for scroll directions.
  */
-public interface Direction {
+public class Direction {
   /** Logical directions */
   public enum LogicalDirection {
-    FORWARD, BACKWARD;
+    FORWARD {
+      @Override
+      public LogicalDirection reverse() {
+        return BACKWARD;
+      }
+    },
+    BACKWARD {
+      @Override
+      public LogicalDirection reverse() {
+        return FORWARD;
+      }
+    };
+    public abstract LogicalDirection reverse();
+  }
+
+  /** Physical directions */
+  public enum PhysicalDirection {
+    UP {
+      @Override
+      public PhysicalDirection reverse() {
+        return DOWN;
+      }
+
+      @Override
+      public Axis axis() {
+        return Axis.VERTICAL;
+      }
+    },
+    DOWN {
+      @Override
+      public PhysicalDirection reverse() {
+        return UP;
+      }
+
+      @Override
+      public Axis axis() {
+        return Axis.VERTICAL;
+      }
+    },
+    LEFT {
+      @Override
+      public PhysicalDirection reverse() {
+        return RIGHT;
+      }
+
+      @Override
+      public Axis axis() {
+        return Axis.HORIZONTAL;
+      }
+    },
+    RIGHT {
+      @Override
+      public PhysicalDirection reverse() {
+        return LEFT;
+      }
+
+      @Override
+      public Axis axis() {
+        return Axis.HORIZONTAL;
+      }
+    };
+    public abstract PhysicalDirection reverse();
+
+    public abstract Axis axis();
   }
 
   public enum Axis {
     HORIZONTAL {
-      private final ScrollDirection[] directions = {ScrollDirection.LEFT, ScrollDirection.RIGHT};
+      private final PhysicalDirection[] directions = {LEFT, RIGHT};
 
       @Override
-      public ScrollDirection[] getDirections() {
+      public PhysicalDirection[] getPhysicalDirections() {
         return directions;
       }
     },
     VERTICAL {
-      private final ScrollDirection[] directions = {ScrollDirection.UP, ScrollDirection.DOWN};
+      private final PhysicalDirection[] directions = {UP, DOWN};
 
       @Override
-      public ScrollDirection[] getDirections() {
+      public PhysicalDirection[] getPhysicalDirections() {
         return directions;
       }
     };
-    public abstract ScrollDirection[] getDirections();
+
+    public abstract PhysicalDirection[] getPhysicalDirections();
   }
 
   /**
-   * Converts ScrollDirection to LogicalDirection.
+   * Converts between PhysicalDirection and LogicalDirection. It's possible to
+   * override this for RTL (right-to-left) views, for example.
    */
-  public interface PhysicalToLogicalConverter {
-    /**
-     * Converts ScrollDirection to LogicalDirection. It's possible to override
-     * this for RTL (right-to-left) views, for example.
-     */
-    LogicalDirection toLogicalDirection(ScrollDirection direction);
+  public static abstract class DirectionConverter {
 
-    /** Follows standard directions: up-to-down, left-to-right */
-    PhysicalToLogicalConverter STANDARD_CONVERTER = new PhysicalToLogicalConverter() {
+    /** Follows standard convention: up-to-down, left-to-right */
+    public static final DirectionConverter STANDARD_CONVERTER = new DirectionConverter() {
       @Override
-      public LogicalDirection toLogicalDirection(ScrollDirection direction) {
-        switch (direction) {
-          case UP:
-            return LogicalDirection.BACKWARD;
-          case DOWN:
-            return LogicalDirection.FORWARD;
-          case LEFT:
-            return LogicalDirection.BACKWARD;
-          case RIGHT:
-            return LogicalDirection.FORWARD;
-        }
-        throw new ActionException("Unknown scroll direction: " + direction);
+      public PhysicalDirection horizontalForwardDirection() {
+        return RIGHT;
+      }
+
+      @Override
+      public PhysicalDirection verticalForwardDirection() {
+        return DOWN;
       }
     };
+
+    /** Follows RTL convention: up-to-down, right-to-left */
+    public static final DirectionConverter RTL_CONVERTER = new DirectionConverter() {
+      @Override
+      public PhysicalDirection horizontalForwardDirection() {
+        return LEFT;
+      }
+
+      @Override
+      public PhysicalDirection verticalForwardDirection() {
+        return DOWN;
+      }
+    };
+
+    public abstract PhysicalDirection horizontalForwardDirection();
+
+    public abstract PhysicalDirection verticalForwardDirection();
+
+    public final PhysicalDirection horizontalBackwardDirection() {
+      return horizontalForwardDirection().reverse();
+    }
+
+    public final PhysicalDirection verticalBackwardDirection() {
+      return verticalForwardDirection().reverse();
+    }
+
+    /** Converts PhysicalDirection to LogicalDirection */
+    public final LogicalDirection toLogicalDirection(PhysicalDirection physicalDirection) {
+      LogicalDirection forward = LogicalDirection.FORWARD;
+      if (toPhysicalDirection(physicalDirection.axis(), forward) == physicalDirection) {
+        return forward;
+      }
+      return forward.reverse();
+    }
+
+    /** Converts LogicalDirection to PhysicalDirection */
+    public final PhysicalDirection toPhysicalDirection(Axis axis, LogicalDirection logicalDirection) {
+      switch (axis) {
+        case HORIZONTAL:
+          switch (logicalDirection) {
+            case BACKWARD:
+              return horizontalBackwardDirection();
+            case FORWARD:
+              return horizontalForwardDirection();
+          }
+          break;
+        case VERTICAL:
+          switch (logicalDirection) {
+            case BACKWARD:
+              return verticalBackwardDirection();
+            case FORWARD:
+              return verticalForwardDirection();
+          }
+      }
+      return null;
+    }
   }
+
+  private Direction() {}
 }
diff --git a/src/com/google/android/droiddriver/scroll/DynamicSentinelStrategy.java b/src/com/google/android/droiddriver/scroll/DynamicSentinelStrategy.java
index c166c1e..b7ccce8 100644
--- a/src/com/google/android/droiddriver/scroll/DynamicSentinelStrategy.java
+++ b/src/com/google/android/droiddriver/scroll/DynamicSentinelStrategy.java
@@ -19,22 +19,22 @@
 
 import com.google.android.droiddriver.DroidDriver;
 import com.google.android.droiddriver.UiElement;
-import com.google.android.droiddriver.actions.ScrollDirection;
 import com.google.android.droiddriver.exceptions.ElementNotFoundException;
+import com.google.android.droiddriver.finders.By;
 import com.google.android.droiddriver.finders.Finder;
-import com.google.android.droiddriver.scroll.Direction.PhysicalToLogicalConverter;
+import com.google.android.droiddriver.scroll.Direction.DirectionConverter;
+import com.google.android.droiddriver.scroll.Direction.PhysicalDirection;
 import com.google.android.droiddriver.util.Logs;
-import com.google.common.base.Objects;
+import com.google.android.droiddriver.util.Strings;
 
 /**
  * Determines whether scrolling is possible by checking whether the sentinel
- * child is updated after scrolling. Use this when
- * {@link UiElement#getChildCount()} is not reliable. This can happen, for
- * instance, when UiAutomationDriver is used, which skips invisible children, or
- * in the case of dynamic list, which shows more items when scrolling beyond the
- * end.
+ * child is updated after scrolling. Use this when {@link UiElement#getChildren}
+ * is not reliable. This can happen, for instance, when UiAutomationDriver is
+ * used, which skips invisible children, or in the case of dynamic list, which
+ * shows more items when scrolling beyond the end.
  */
-public class DynamicSentinelStrategy extends AbstractSentinelStrategy {
+public class DynamicSentinelStrategy extends SentinelStrategy {
 
   /**
    * Interface for determining whether sentinel is updated.
@@ -58,25 +58,25 @@
 
   /**
    * Determines whether the sentinel is updated by checking a single unique
-   * String attribute of a child element of the sentinel (or itself).
+   * String attribute of a descendant element of the sentinel (or itself).
    */
   public static abstract class SingleStringUpdated implements IsUpdatedStrategy {
     private final Finder uniqueStringFinder;
 
     /**
      * @param uniqueStringFinder a Finder relative to the sentinel that finds
-     *        its child element which contains a unique String.
+     *        its descendant or self which contains a unique String.
      */
     public SingleStringUpdated(Finder uniqueStringFinder) {
       this.uniqueStringFinder = uniqueStringFinder;
     }
 
     /**
-     * @param uniqueStringChild the child of sentinel (or itself) that contains
-     *        the unique String
+     * @param uniqueStringElement the descendant or self that contains the
+     *        unique String
      * @return the unique String
      */
-    protected abstract String getUniqueString(UiElement uniqueStringChild);
+    protected abstract String getUniqueString(UiElement uniqueStringElement);
 
     private String getUniqueStringFromSentinel(UiElement sentinel) {
       try {
@@ -88,12 +88,24 @@
 
     @Override
     public boolean isSentinelUpdated(UiElement newSentinel, UiElement oldSentinel) {
+      // If the sentinel moved, scrolling has some effect. This is both an
+      // optimization - getBounds is cheaper than find - and necessary in
+      // certain cases, e.g. user is looking for a sibling of the unique string;
+      // the scroll is close to the end therefore the unique string does not
+      // change, but the target could be revealed.
+      if (!newSentinel.getBounds().equals(oldSentinel.getBounds())) {
+        return true;
+      }
+
       String newString = getUniqueStringFromSentinel(newSentinel);
-      // If newString is null, newSentinel must be partially shown. In this case
-      // we return true to allow further scrolling. But program error could also
-      // cause this, e.g. a bad choice of GetStrategy. log for debugging.
+      // A legitimate case for newString being null is when newSentinel is
+      // partially shown. We return true to allow further scrolling. But program
+      // error could also cause this, e.g. a bad choice of Getter, which
+      // results in unnecessary scroll actions that have no visual effect. This
+      // log helps troubleshooting in the latter case.
       if (newString == null) {
-        Logs.logfmt(Log.WARN, "Unique String under sentinel %s is null", newSentinel);
+        Logs.logfmt(Log.WARN, "Unique String is null: sentinel=%s, uniqueStringFinder=%s",
+            newSentinel, uniqueStringFinder);
         return true;
       }
       if (newString.equals(getUniqueStringFromSentinel(oldSentinel))) {
@@ -105,13 +117,13 @@
 
     @Override
     public String toString() {
-      return Objects.toStringHelper(this).addValue(uniqueStringFinder).toString();
+      return Strings.toStringHelper(this).addValue(uniqueStringFinder).toString();
     }
   }
 
   /**
-   * Determines whether the sentinel is updated by checking the text of a child
-   * element of the sentinel (or itself).
+   * Determines whether the sentinel is updated by checking the text of a
+   * descendant element of the sentinel (or itself).
    */
   public static class TextUpdated extends SingleStringUpdated {
     public TextUpdated(Finder uniqueStringFinder) {
@@ -119,14 +131,14 @@
     }
 
     @Override
-    protected String getUniqueString(UiElement uniqueStringChild) {
-      return uniqueStringChild.getText();
+    protected String getUniqueString(UiElement uniqueStringElement) {
+      return uniqueStringElement.getText();
     }
   }
 
   /**
    * Determines whether the sentinel is updated by checking the content
-   * description of a child element of the sentinel (or itself).
+   * description of a descendant element of the sentinel (or itself).
    */
   public static class ContentDescriptionUpdated extends SingleStringUpdated {
     public ContentDescriptionUpdated(Finder uniqueStringFinder) {
@@ -134,57 +146,93 @@
     }
 
     @Override
-    protected String getUniqueString(UiElement uniqueStringChild) {
-      return uniqueStringChild.getContentDescription();
+    protected String getUniqueString(UiElement uniqueStringElement) {
+      return uniqueStringElement.getContentDescription();
+    }
+  }
+
+  /**
+   * Determines whether the sentinel is updated by checking the resource-id of a
+   * descendant element of the sentinel (often itself). This is useful when the
+   * children of the container are heterogeneous -- they don't have a common
+   * pattern to get a unique string.
+   */
+  public static class ResourceIdUpdated extends SingleStringUpdated {
+    /**
+     * Uses the resource-id of the sentinel itself.
+     */
+    public static final ResourceIdUpdated SELF = new ResourceIdUpdated(By.any());
+
+    public ResourceIdUpdated(Finder uniqueStringFinder) {
+      super(uniqueStringFinder);
+    }
+
+    @Override
+    protected String getUniqueString(UiElement uniqueStringElement) {
+      return uniqueStringElement.getResourceId();
     }
   }
 
   private final IsUpdatedStrategy isUpdatedStrategy;
+  private UiElement lastSentinel;
 
   /**
-   * Constructs with {@code GetStrategy}s that decorate the given
-   * {@code GetStrategy}s with {@link UiElement#VISIBLE}, and the given
-   * {@code isUpdatedStrategy} and {@code physicalToLogicalConverter}. Be
-   * careful with {@code GetStrategy}s: the sentinel after each scroll should be
-   * unique.
+   * Constructs with {@code Getter}s that decorate the given {@code Getter}s
+   * with {@link UiElement#VISIBLE}, and the given {@code isUpdatedStrategy} and
+   * {@code directionConverter}. Be careful with {@code Getter}s: the sentinel
+   * after each scroll should be unique.
    */
-  public DynamicSentinelStrategy(IsUpdatedStrategy isUpdatedStrategy,
-      GetStrategy backwardGetStrategy, GetStrategy forwardGetStrategy,
-      PhysicalToLogicalConverter physicalToLogicalConverter) {
-    super(new MorePredicateGetStrategy(backwardGetStrategy, UiElement.VISIBLE, "VISIBLE_"),
-        new MorePredicateGetStrategy(forwardGetStrategy, UiElement.VISIBLE, "VISIBLE_"),
-        physicalToLogicalConverter);
+  public DynamicSentinelStrategy(IsUpdatedStrategy isUpdatedStrategy, Getter backwardGetter,
+      Getter forwardGetter, DirectionConverter directionConverter) {
+    super(new MorePredicateGetter(backwardGetter, UiElement.VISIBLE), new MorePredicateGetter(
+        forwardGetter, UiElement.VISIBLE), directionConverter);
     this.isUpdatedStrategy = isUpdatedStrategy;
   }
 
   /**
-   * Defaults to the standard {@link PhysicalToLogicalConverter}.
+   * Defaults to the standard {@link DirectionConverter}.
    */
-  public DynamicSentinelStrategy(IsUpdatedStrategy isUpdatedStrategy,
-      GetStrategy backwardGetStrategy, GetStrategy forwardGetStrategy) {
-    this(isUpdatedStrategy, backwardGetStrategy, forwardGetStrategy,
-        PhysicalToLogicalConverter.STANDARD_CONVERTER);
+  public DynamicSentinelStrategy(IsUpdatedStrategy isUpdatedStrategy, Getter backwardGetter,
+      Getter forwardGetter) {
+    this(isUpdatedStrategy, backwardGetter, forwardGetter, DirectionConverter.STANDARD_CONVERTER);
   }
 
   /**
    * Defaults to LAST_CHILD_GETTER for forward scrolling, and the standard
-   * {@link PhysicalToLogicalConverter}.
+   * {@link DirectionConverter}.
    */
-  public DynamicSentinelStrategy(IsUpdatedStrategy isUpdatedStrategy,
-      GetStrategy backwardGetStrategy) {
-    this(isUpdatedStrategy, backwardGetStrategy, LAST_CHILD_GETTER,
-        PhysicalToLogicalConverter.STANDARD_CONVERTER);
+  public DynamicSentinelStrategy(IsUpdatedStrategy isUpdatedStrategy, Getter backwardGetter) {
+    this(isUpdatedStrategy, backwardGetter, LAST_CHILD_GETTER,
+        DirectionConverter.STANDARD_CONVERTER);
   }
 
   @Override
-  public boolean scroll(DroidDriver driver, Finder parentFinder, ScrollDirection direction) {
-    UiElement parent = driver.on(parentFinder);
-    UiElement oldSentinel = getSentinel(parent, direction);
-    parent.scroll(direction);
-    UiElement newSentinel = getSentinel(driver.on(parentFinder), direction);
+  public boolean scroll(DroidDriver driver, Finder containerFinder, PhysicalDirection direction) {
+    UiElement oldSentinel = getOldSentinel(driver, containerFinder, direction);
+    doScroll(oldSentinel.getParent(), direction);
+    UiElement newSentinel = getSentinel(driver, containerFinder, direction);
+    lastSentinel = newSentinel;
     return isUpdatedStrategy.isSentinelUpdated(newSentinel, oldSentinel);
   }
 
+  private UiElement getOldSentinel(DroidDriver driver, Finder containerFinder,
+      PhysicalDirection direction) {
+    return lastSentinel != null ? lastSentinel : getSentinel(driver, containerFinder, direction);
+  }
+
+  @Override
+  public void beginScrolling(DroidDriver driver, Finder containerFinder, Finder itemFinder,
+      PhysicalDirection direction) {
+    lastSentinel = null;
+  }
+
+  @Override
+  public void endScrolling(DroidDriver driver, Finder containerFinder, Finder itemFinder,
+      PhysicalDirection direction) {
+    // Prevent memory leak
+    lastSentinel = null;
+  }
+
   @Override
   public String toString() {
     return String.format("DynamicSentinelStrategy{%s, isUpdatedStrategy=%s}", super.toString(),
diff --git a/src/com/google/android/droiddriver/scroll/ForwardingScrollStepStrategy.java b/src/com/google/android/droiddriver/scroll/ForwardingScrollStepStrategy.java
new file mode 100644
index 0000000..bf3bc24
--- /dev/null
+++ b/src/com/google/android/droiddriver/scroll/ForwardingScrollStepStrategy.java
@@ -0,0 +1,64 @@
+/*
+ * 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.scroll;
+
+
+import com.google.android.droiddriver.DroidDriver;
+import com.google.android.droiddriver.UiElement;
+import com.google.android.droiddriver.finders.Finder;
+import com.google.android.droiddriver.scroll.Direction.DirectionConverter;
+import com.google.android.droiddriver.scroll.Direction.PhysicalDirection;
+
+/**
+ * An abstract base class for implementing the <a
+ * href="http://en.wikipedia.org/wiki/Decorator_pattern">decorator pattern</a>,
+ * forwarding calls to all methods of {@link ScrollStepStrategy} to delegate.
+ */
+public abstract class ForwardingScrollStepStrategy implements ScrollStepStrategy {
+
+  protected ForwardingScrollStepStrategy() {}
+
+  /**
+   * Returns the backing delegate instance that methods are forwarded to.
+   */
+  protected abstract ScrollStepStrategy delegate();
+
+  public boolean scroll(DroidDriver driver, Finder containerFinder, PhysicalDirection direction) {
+    return delegate().scroll(driver, containerFinder, direction);
+  }
+
+  @Override
+  public final DirectionConverter getDirectionConverter() {
+    return delegate().getDirectionConverter();
+  }
+
+  @Override
+  public void beginScrolling(DroidDriver driver, Finder containerFinder, Finder itemFinder,
+      PhysicalDirection direction) {
+    delegate().beginScrolling(driver, containerFinder, itemFinder, direction);
+  }
+
+  @Override
+  public void endScrolling(DroidDriver driver, Finder containerFinder, Finder itemFinder,
+      PhysicalDirection direction) {
+    delegate().endScrolling(driver, containerFinder, itemFinder, direction);
+  }
+
+  @Override
+  public void doScroll(UiElement container, PhysicalDirection direction) {
+    delegate().doScroll(container, direction);
+  }
+}
diff --git a/src/com/google/android/droiddriver/scroll/ScrollStepStrategy.java b/src/com/google/android/droiddriver/scroll/ScrollStepStrategy.java
new file mode 100644
index 0000000..9583def
--- /dev/null
+++ b/src/com/google/android/droiddriver/scroll/ScrollStepStrategy.java
@@ -0,0 +1,91 @@
+/*
+ * 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.scroll;
+
+import com.google.android.droiddriver.DroidDriver;
+import com.google.android.droiddriver.UiElement;
+import com.google.android.droiddriver.finders.Finder;
+import com.google.android.droiddriver.scroll.Direction.DirectionConverter;
+import com.google.android.droiddriver.scroll.Direction.PhysicalDirection;
+
+/**
+ * Interface for determining whether scrolling is possible.
+ */
+public interface ScrollStepStrategy {
+  /**
+   * Tries to scroll {@code containerFinder} in {@code direction}. Returns
+   * whether scrolling is effective.
+   *
+   * @param driver
+   * @param containerFinder Finder for the container that can scroll, for
+   *        instance a ListView
+   * @param direction
+   * @return whether scrolling is effective
+   */
+  boolean scroll(DroidDriver driver, Finder containerFinder, PhysicalDirection direction);
+
+  /**
+   * Returns the {@link DirectionConverter}.
+   */
+  DirectionConverter getDirectionConverter();
+
+  /**
+   * Called only if this step is at the beginning of a series of scroll steps
+   * with regard to the given arguments.
+   *
+   * @param driver
+   * @param containerFinder Finder for the container that can scroll, for
+   *        instance a ListView
+   * @param itemFinder Finder for the desired item; relative to
+   *        {@code containerFinder}
+   * @param direction
+   */
+  void beginScrolling(DroidDriver driver, Finder containerFinder, Finder itemFinder,
+      PhysicalDirection direction);
+
+  /**
+   * Called only if this step is at the end of a series of scroll steps with
+   * regard to the given arguments.
+   *
+   * @param driver
+   * @param containerFinder Finder for the container that can scroll, for
+   *        instance a ListView
+   * @param itemFinder Finder for the desired item; relative to
+   *        {@code containerFinder}
+   * @param direction
+   */
+  void endScrolling(DroidDriver driver, Finder containerFinder, Finder itemFinder,
+      PhysicalDirection direction);
+
+  /**
+   * Performs the scroll action on {@code container}. Subclasses can override
+   * this to customize the scroll action, for example, to adjust the scroll
+   * margins.
+   *
+   * @param container the container that can scroll
+   * @param direction
+   */
+  void doScroll(UiElement container, PhysicalDirection direction);
+
+  /**
+   * {@inheritDoc}
+   *
+   * <p>
+   * It is recommended that this method return a description to help debugging.
+   */
+  @Override
+  String toString();
+}
diff --git a/src/com/google/android/droiddriver/scroll/Scroller.java b/src/com/google/android/droiddriver/scroll/Scroller.java
index 538e7a9..f94f784 100644
--- a/src/com/google/android/droiddriver/scroll/Scroller.java
+++ b/src/com/google/android/droiddriver/scroll/Scroller.java
@@ -17,41 +17,41 @@
 
 import com.google.android.droiddriver.DroidDriver;
 import com.google.android.droiddriver.UiElement;
-import com.google.android.droiddriver.actions.ScrollDirection;
 import com.google.android.droiddriver.exceptions.ElementNotFoundException;
 import com.google.android.droiddriver.finders.Finder;
+import com.google.android.droiddriver.scroll.Direction.PhysicalDirection;
 
 /**
- * Interface for scrolling to the desired child in a scrollable parent view.
+ * Interface for scrolling to the desired item in a scrollable container view.
  */
 public interface Scroller {
   /**
-   * Scrolls {@code parentFinder} in both directions if necessary to find
-   * {@code childFinder}.
+   * Scrolls {@code containerFinder} in both directions if necessary to find
+   * {@code itemFinder}, which is a descendant of {@code containerFinder}.
    *
    * @param driver
-   * @param parentFinder Finder for the container that can scroll, for instance
-   *        a ListView
-   * @param childFinder Finder for the desired child; relative to
-   *        {@code parentFinder}
-   * @return the UiElement matching {@code childFinder}
+   * @param containerFinder Finder for the container that can scroll, for
+   *        instance a ListView
+   * @param itemFinder Finder for the desired item; relative to
+   *        {@code containerFinder}
+   * @return the UiElement matching {@code itemFinder}
    * @throws ElementNotFoundException If no match is found
    */
-  UiElement scrollTo(DroidDriver driver, Finder parentFinder, Finder childFinder);
+  UiElement scrollTo(DroidDriver driver, Finder containerFinder, Finder itemFinder);
 
   /**
-   * Scrolls {@code parentFinder} in {@code direction} if necessary to find
-   * {@code childFinder}.
+   * Scrolls {@code containerFinder} in {@code direction} if necessary to find
+   * {@code itemFinder}, which is a descendant of {@code containerFinder}.
    *
    * @param driver
-   * @param parentFinder Finder for the container that can scroll, for instance
-   *        a ListView
-   * @param childFinder Finder for the desired child; relative to
-   *        {@code parentFinder}
+   * @param containerFinder Finder for the container that can scroll, for
+   *        instance a ListView
+   * @param itemFinder Finder for the desired item; relative to
+   *        {@code containerFinder}
    * @param direction
-   * @return the UiElement matching {@code childFinder}
+   * @return the UiElement matching {@code itemFinder}
    * @throws ElementNotFoundException If no match is found
    */
-  UiElement scrollTo(DroidDriver driver, Finder parentFinder, Finder childFinder,
-      ScrollDirection direction);
+  UiElement scrollTo(DroidDriver driver, Finder containerFinder, Finder itemFinder,
+      PhysicalDirection direction);
 }
diff --git a/src/com/google/android/droiddriver/scroll/Scrollers.java b/src/com/google/android/droiddriver/scroll/Scrollers.java
new file mode 100644
index 0000000..3459f0e
--- /dev/null
+++ b/src/com/google/android/droiddriver/scroll/Scrollers.java
@@ -0,0 +1,87 @@
+/*
+ * 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.scroll;
+
+import android.app.UiAutomation;
+import android.widget.ProgressBar;
+
+import com.google.android.droiddriver.DroidDriver;
+import com.google.android.droiddriver.finders.By;
+import com.google.android.droiddriver.finders.Finder;
+import com.google.android.droiddriver.scroll.Direction.Axis;
+import com.google.android.droiddriver.scroll.Direction.DirectionConverter;
+import com.google.android.droiddriver.scroll.Direction.PhysicalDirection;
+
+/**
+ * Static utility classes and methods pertaining to {@link Scroller} instances.
+ */
+public class Scrollers {
+  /**
+   * Augments the delegate {@link ScrollStepStrategy) - after a successful
+   * scroll, waits until ProgressBar is gone.
+   */
+  public static abstract class ProgressBarScrollStepStrategy extends ForwardingScrollStepStrategy {
+    @Override
+    public boolean scroll(DroidDriver driver, Finder containerFinder, PhysicalDirection direction) {
+      if (super.scroll(driver, containerFinder, direction)) {
+        driver.checkGone(By.className(ProgressBar.class));
+        return true;
+      }
+      return false;
+    }
+
+    /** Convenience method to wrap {@code delegate} with this class */
+    public static ScrollStepStrategy wrap(final ScrollStepStrategy delegate) {
+      return new ProgressBarScrollStepStrategy() {
+        @Override
+        protected ScrollStepStrategy delegate() {
+          return delegate;
+        }
+      };
+    }
+  }
+
+  /**
+   * Returns a new default Scroller that works in simple cases. In complex cases
+   * you may try a {@link StepBasedScroller} with a custom
+   * {@link ScrollStepStrategy}:
+   * <ul>
+   * <li>If the Scroller is used with InstrumentationDriver,
+   * StaticSentinelStrategy may work and it's the simplest.</li>
+   * <li>Otherwise, DynamicSentinelStrategy should work in all cases, including
+   * the case of dynamic list, which shows more items when scrolling beyond the
+   * end. On the other hand, it's complex and needs more configuration.</li>
+   * </ul>
+   * Note if a {@link StepBasedScroller} is returned, it is constructed with
+   * arguments that apply to typical cases. You may want to customize them for
+   * specific cases. For instance, {@code perScrollTimeoutMillis} can be 0L if
+   * there are no asynchronously updated views. To that extent, this method
+   * serves as an example of how to construct {@link Scroller}s rather than
+   * providing the "official" {@link Scroller}.
+   */
+  public static Scroller newScroller(UiAutomation uiAutomation) {
+    if (uiAutomation != null) {
+      return new StepBasedScroller(100/* maxScrolls */, 1000L/* perScrollTimeoutMillis */,
+          Axis.VERTICAL, new AccessibilityEventScrollStepStrategy(uiAutomation, 1000L,
+              DirectionConverter.STANDARD_CONVERTER), true/* startFromBeginning */);
+    }
+    // TODO: A {@link Scroller} that directly jumps to the view if an
+    // InstrumentationDriver is used.
+    return new StepBasedScroller(100/* maxScrolls */, 1000L/* perScrollTimeoutMillis */,
+        Axis.VERTICAL, StaticSentinelStrategy.DEFAULT, true/* startFromBeginning */);
+  }
+}
diff --git a/src/com/google/android/droiddriver/scroll/SentinelScroller.java b/src/com/google/android/droiddriver/scroll/SentinelScroller.java
deleted file mode 100644
index 746ad6d..0000000
--- a/src/com/google/android/droiddriver/scroll/SentinelScroller.java
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * 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.scroll;
-
-import android.util.Log;
-
-import com.google.android.droiddriver.DroidDriver;
-import com.google.android.droiddriver.Poller;
-import com.google.android.droiddriver.UiElement;
-import com.google.android.droiddriver.actions.ScrollDirection;
-import com.google.android.droiddriver.exceptions.ElementNotFoundException;
-import com.google.android.droiddriver.exceptions.TimeoutException;
-import com.google.android.droiddriver.finders.Finder;
-import com.google.android.droiddriver.scroll.Direction.Axis;
-import com.google.android.droiddriver.util.Logs;
-
-/**
- * A {@link Scroller} that looks for the desired child in the current shown
- * content of the parent, otherwise scrolls the parent one step at a time and
- * look again, until we cannot scroll any more. A {@link SentinelStrategy} is
- * used to determine whether more scrolling is possible.
- * <p>
- * This algorithm is needed unless the DroidDriver implementation supports
- * directly jumping to the child.
- */
-public class SentinelScroller implements Scroller {
-  private final int maxScrolls;
-  private final long perScrollTimeoutMillis;
-  private final Axis axis;
-  private final SentinelStrategy sentinelStrategy;
-
-  /**
-   * @param maxScrolls the maximum number of scrolls. It should be large enough
-   *        to allow any reasonable list size
-   * @param perScrollTimeoutMillis the timeout in millis that we poll for the
-   *        child after each scroll
-   * @param axis the axis this scroller can scroll
-   */
-  public SentinelScroller(int maxScrolls, long perScrollTimeoutMillis, Axis axis,
-      SentinelStrategy sentinelStrategy) {
-    this.maxScrolls = maxScrolls;
-    this.perScrollTimeoutMillis = perScrollTimeoutMillis;
-    this.axis = axis;
-    this.sentinelStrategy = sentinelStrategy;
-  }
-
-  /**
-   * Constructs with default 100 maxScrolls, 1 second for
-   * perScrollTimeoutMillis, vertical axis.
-   */
-  public SentinelScroller(SentinelStrategy sentinelStrategy) {
-    this(100, 1000, Axis.VERTICAL, sentinelStrategy);
-  }
-
-  @Override
-  public UiElement scrollTo(DroidDriver driver, Finder parentFinder, Finder childFinder,
-      ScrollDirection direction) {
-    Logs.call(this, "scrollTo", driver, parentFinder, childFinder, direction);
-    // TODO: enforce childFinder is relative to parentFinder.
-    // Combine with parentFinder to make childFinder absolute
-    // childFinder = By.chain(parentFinder, childFinder);
-
-    int i = 0;
-    for (; i <= maxScrolls; i++) {
-      try {
-        return driver.getPoller().pollFor(driver, childFinder, Poller.EXISTS,
-            perScrollTimeoutMillis);
-      } catch (TimeoutException e) {
-        if (i < maxScrolls && !sentinelStrategy.scroll(driver, parentFinder, direction)) {
-          break;
-        }
-      }
-    }
-
-    ElementNotFoundException exception = new ElementNotFoundException(childFinder);
-    if (i == maxScrolls) {
-      // This is often a program error -- maxScrolls is a safety net; we should
-      // have either found childFinder, or stopped to scroll b/c of reaching the
-      // end. If maxScrolls is reasonably large, sentinelStrategy must be wrong.
-      Logs.logfmt(Log.WARN, exception, "Scrolled %s %d times; sentinelStrategy=%s", parentFinder,
-          maxScrolls, sentinelStrategy);
-    }
-    throw exception;
-  }
-
-  @Override
-  public UiElement scrollTo(DroidDriver driver, Finder parentFinder, Finder childFinder) {
-    // TODO: start searching from beginning instead of the current location.
-    for (ScrollDirection direction : axis.getDirections()) {
-      try {
-        return scrollTo(driver, parentFinder, childFinder, direction);
-      } catch (ElementNotFoundException e) {
-        // try another direction
-      }
-    }
-    throw new ElementNotFoundException(childFinder);
-  }
-}
diff --git a/src/com/google/android/droiddriver/scroll/SentinelStrategy.java b/src/com/google/android/droiddriver/scroll/SentinelStrategy.java
index 3b8edec..19c1258 100644
--- a/src/com/google/android/droiddriver/scroll/SentinelStrategy.java
+++ b/src/com/google/android/droiddriver/scroll/SentinelStrategy.java
@@ -15,32 +15,196 @@
  */
 package com.google.android.droiddriver.scroll;
 
+import android.util.Log;
+
 import com.google.android.droiddriver.DroidDriver;
-import com.google.android.droiddriver.actions.ScrollDirection;
+import com.google.android.droiddriver.UiElement;
+import com.google.android.droiddriver.exceptions.ElementNotFoundException;
+import com.google.android.droiddriver.finders.By;
 import com.google.android.droiddriver.finders.Finder;
+import com.google.android.droiddriver.finders.Predicate;
+import com.google.android.droiddriver.finders.Predicates;
+import com.google.android.droiddriver.scroll.Direction.DirectionConverter;
+import com.google.android.droiddriver.scroll.Direction.LogicalDirection;
+import com.google.android.droiddriver.scroll.Direction.PhysicalDirection;
+import com.google.android.droiddriver.util.Logs;
+
+import java.util.List;
 
 /**
- * Interface for determining whether scrolling is possible based on a sentinel.
+ * A {@link ScrollStepStrategy} that determines whether scrolling is possible
+ * based on a sentinel.
  */
-public interface SentinelStrategy {
+public abstract class SentinelStrategy implements ScrollStepStrategy {
   /**
-   * Tries to scroll {@code parentFinder} in {@code direction}. Returns whether
-   * scrolling is effective.
-   *
-   * @param driver
-   * @param parentFinder Finder for the container that can scroll, for instance
-   *        a ListView
-   * @param direction
-   * @return whether scrolling is effective
+   * A {@link Finder} for sentinel. Note that unlike {@link Finder}, invisible
+   * UiElements are not skipped by default.
    */
-  boolean scroll(DroidDriver driver, Finder parentFinder, ScrollDirection direction);
+  public static abstract class Getter implements Finder {
+    protected final Predicate<? super UiElement> predicate;
+
+    protected Getter() {
+      // Include invisible children by default.
+      this(null);
+    }
+
+    protected Getter(Predicate<? super UiElement> predicate) {
+      this.predicate = predicate;
+    }
+
+    /**
+     * Gets the sentinel, which must be an immediate child of {@code container}
+     * - not a descendant. Note sentinel may not exist if {@code container} has
+     * not finished updating.
+     */
+    @Override
+    public UiElement find(UiElement container) {
+      UiElement sentinel = getSentinel(container.getChildren(predicate));
+      if (sentinel == null) {
+        throw new ElementNotFoundException(this);
+      }
+      Logs.log(Log.INFO, "Found sentinel: " + sentinel);
+      return sentinel;
+    }
+
+
+    protected abstract UiElement getSentinel(List<? extends UiElement> children);
+
+    @Override
+    public abstract String toString();
+  }
 
   /**
-   * {@inheritDoc}
-   *
-   * <p>
-   * It is recommended that this method return a description to help debugging.
+   * Returns the first child as the sentinel.
    */
+  public static final Getter FIRST_CHILD_GETTER = new Getter() {
+    @Override
+    protected UiElement getSentinel(List<? extends UiElement> children) {
+      return children.isEmpty() ? null : children.get(0);
+    }
+
+    @Override
+    public String toString() {
+      return "FIRST_CHILD";
+    }
+  };
+  /**
+   * Returns the last child as the sentinel.
+   */
+  public static final Getter LAST_CHILD_GETTER = new Getter() {
+    @Override
+    protected UiElement getSentinel(List<? extends UiElement> children) {
+      return children.isEmpty() ? null : children.get(children.size() - 1);
+    }
+
+    @Override
+    public String toString() {
+      return "LAST_CHILD";
+    }
+  };
+  /**
+   * Returns the second last child as the sentinel. Useful when the activity
+   * always shows the last child as an anchor (for example a footer).
+   * <p>
+   * Sometimes uiautomatorviewer may not show the anchor as the last child, due
+   * to the reordering by layout described in {@link UiElement#getChildren}.
+   * This is not a problem with UiAutomationDriver because it sees the same as
+   * uiautomatorviewer does, but could be a problem with InstrumentationDriver.
+   * </p>
+   */
+  public static final Getter SECOND_LAST_CHILD_GETTER = new Getter() {
+    @Override
+    protected UiElement getSentinel(List<? extends UiElement> children) {
+      return children.size() < 2 ? null : children.get(children.size() - 2);
+    }
+
+    @Override
+    public String toString() {
+      return "SECOND_LAST_CHILD";
+    }
+  };
+  /**
+   * Returns the second child as the sentinel. Useful when the activity shows a
+   * fixed first child.
+   */
+  public static final Getter SECOND_CHILD_GETTER = new Getter() {
+    @Override
+    protected UiElement getSentinel(List<? extends UiElement> children) {
+      return children.size() <= 1 ? null : children.get(1);
+    }
+
+    @Override
+    public String toString() {
+      return "SECOND_CHILD";
+    }
+  };
+
+  /**
+   * Decorates a {@link Getter} by adding another {@link Predicate}.
+   */
+  public static class MorePredicateGetter extends Getter {
+    private final Getter original;
+
+    public MorePredicateGetter(Getter original, Predicate<? super UiElement> extraPredicate) {
+      super(Predicates.allOf(original.predicate, extraPredicate));
+      this.original = original;
+    }
+
+    @Override
+    protected UiElement getSentinel(List<? extends UiElement> children) {
+      return original.getSentinel(children);
+    }
+
+    @Override
+    public String toString() {
+      return predicate.toString() + " " + original;
+    }
+  }
+
+  private final Getter backwardGetter;
+  private final Getter forwardGetter;
+  private final DirectionConverter directionConverter;
+
+  protected SentinelStrategy(Getter backwardGetter, Getter forwardGetter,
+      DirectionConverter directionConverter) {
+    this.backwardGetter = backwardGetter;
+    this.forwardGetter = forwardGetter;
+    this.directionConverter = directionConverter;
+  }
+
+  protected UiElement getSentinel(DroidDriver driver, Finder containerFinder,
+      PhysicalDirection direction) {
+    Logs.call(this, "getSentinel", driver, containerFinder, direction);
+    Finder sentinelFinder;
+    LogicalDirection logicalDirection = directionConverter.toLogicalDirection(direction);
+    if (logicalDirection == LogicalDirection.BACKWARD) {
+      sentinelFinder = By.chain(containerFinder, backwardGetter);
+    } else {
+      sentinelFinder = By.chain(containerFinder, forwardGetter);
+    }
+    return driver.on(sentinelFinder);
+  }
+
   @Override
-  String toString();
+  public final DirectionConverter getDirectionConverter() {
+    return directionConverter;
+  }
+
+  @Override
+  public void beginScrolling(DroidDriver driver, Finder containerFinder, Finder itemFinder,
+      PhysicalDirection direction) {}
+
+  @Override
+  public void endScrolling(DroidDriver driver, Finder containerFinder, Finder itemFinder,
+      PhysicalDirection direction) {}
+
+  @Override
+  public String toString() {
+    return String.format("{backwardGetter=%s, forwardGetter=%s}", backwardGetter, forwardGetter);
+  }
+
+  @Override
+  public void doScroll(UiElement container, PhysicalDirection direction) {
+    container.scroll(direction);
+  }
 }
diff --git a/src/com/google/android/droiddriver/scroll/StaticSentinelStrategy.java b/src/com/google/android/droiddriver/scroll/StaticSentinelStrategy.java
index 9b25a5c..4cd3bc7 100644
--- a/src/com/google/android/droiddriver/scroll/StaticSentinelStrategy.java
+++ b/src/com/google/android/droiddriver/scroll/StaticSentinelStrategy.java
@@ -19,43 +19,46 @@
 
 import com.google.android.droiddriver.DroidDriver;
 import com.google.android.droiddriver.UiElement;
-import com.google.android.droiddriver.actions.ScrollDirection;
 import com.google.android.droiddriver.finders.Finder;
 import com.google.android.droiddriver.instrumentation.InstrumentationDriver;
-import com.google.android.droiddriver.scroll.Direction.PhysicalToLogicalConverter;
+import com.google.android.droiddriver.scroll.Direction.DirectionConverter;
+import com.google.android.droiddriver.scroll.Direction.PhysicalDirection;
 
 /**
  * Determines whether scrolling is possible by checking whether the last child
- * in the logical scroll direction is fully visible. Use this when the count of
- * children is static, and {@link UiElement#getChildCount()} includes all
- * children no matter if it is visible. Currently {@link InstrumentationDriver}
- * behaves this way.
+ * in the logical scroll direction is fully visible. This assumes the count of
+ * children is static, and {@link UiElement#getChildren} includes all children
+ * no matter if it is visible. Currently {@link InstrumentationDriver} behaves
+ * this way.
+ * <p>
+ * This does not work if a child is larger than the physical size of the
+ * container.
  */
-public class StaticSentinelStrategy extends AbstractSentinelStrategy {
+public class StaticSentinelStrategy extends SentinelStrategy {
   /**
    * Defaults to FIRST_CHILD_GETTER for backward scrolling, LAST_CHILD_GETTER
-   * for forward scrolling, and the standard {@link PhysicalToLogicalConverter}.
+   * for forward scrolling, and the standard {@link DirectionConverter}.
    */
-  public StaticSentinelStrategy() {
-    super(FIRST_CHILD_GETTER, LAST_CHILD_GETTER, PhysicalToLogicalConverter.STANDARD_CONVERTER);
-  }
+  public static final StaticSentinelStrategy DEFAULT = new StaticSentinelStrategy(
+      FIRST_CHILD_GETTER, LAST_CHILD_GETTER, DirectionConverter.STANDARD_CONVERTER);
 
-  public StaticSentinelStrategy(GetStrategy backwardGetStrategy, GetStrategy forwardGetStrategy,
-      PhysicalToLogicalConverter physicalToLogicalConverter) {
-    super(backwardGetStrategy, forwardGetStrategy, physicalToLogicalConverter);
+  public StaticSentinelStrategy(Getter backwardGetter, Getter forwardGetter,
+      DirectionConverter directionConverter) {
+    super(backwardGetter, forwardGetter, directionConverter);
   }
 
   @Override
-  public boolean scroll(DroidDriver driver, Finder parentFinder, ScrollDirection direction) {
-    UiElement parent = driver.on(parentFinder);
+  public boolean scroll(DroidDriver driver, Finder containerFinder, PhysicalDirection direction) {
+    UiElement sentinel = getSentinel(driver, containerFinder, direction);
+    UiElement container = sentinel.getParent();
     // If the last child in the logical scroll direction is fully visible, no
     // more scrolling is possible
-    Rect visibleBounds = parent.getVisibleBounds();
-    if (visibleBounds.contains(getSentinel(parent, direction).getBounds())) {
+    Rect visibleBounds = container.getVisibleBounds();
+    if (visibleBounds.contains(sentinel.getBounds())) {
       return false;
     }
 
-    parent.scroll(direction);
+    doScroll(container, direction);
     return true;
   }
 }
diff --git a/src/com/google/android/droiddriver/scroll/StepBasedScroller.java b/src/com/google/android/droiddriver/scroll/StepBasedScroller.java
new file mode 100644
index 0000000..d962ed3
--- /dev/null
+++ b/src/com/google/android/droiddriver/scroll/StepBasedScroller.java
@@ -0,0 +1,168 @@
+/*
+ * 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.scroll;
+
+import static com.google.android.droiddriver.scroll.Direction.LogicalDirection.BACKWARD;
+
+import android.util.Log;
+
+import com.google.android.droiddriver.DroidDriver;
+import com.google.android.droiddriver.Poller;
+import com.google.android.droiddriver.UiElement;
+import com.google.android.droiddriver.exceptions.ElementNotFoundException;
+import com.google.android.droiddriver.exceptions.TimeoutException;
+import com.google.android.droiddriver.finders.By;
+import com.google.android.droiddriver.finders.Finder;
+import com.google.android.droiddriver.scroll.Direction.Axis;
+import com.google.android.droiddriver.scroll.Direction.DirectionConverter;
+import com.google.android.droiddriver.scroll.Direction.PhysicalDirection;
+import com.google.android.droiddriver.util.Logs;
+
+/**
+ * A {@link Scroller} that looks for the desired item in the currently shown
+ * content of the scrollable container, otherwise scrolls the container one step
+ * at a time and looks again, until we cannot scroll any more. A
+ * {@link ScrollStepStrategy} is used to determine whether more scrolling is
+ * possible.
+ */
+public class StepBasedScroller implements Scroller {
+  private final int maxScrolls;
+  private final long perScrollTimeoutMillis;
+  private final Axis axis;
+  private final ScrollStepStrategy scrollStepStrategy;
+  private final boolean startFromBeginning;
+
+  /**
+   * @param maxScrolls the maximum number of scrolls. It should be large enough
+   *        to allow any reasonable list size
+   * @param perScrollTimeoutMillis the timeout in millis that we poll for the
+   *        item after each scroll. 1000L is usually safe; if there are no
+   *        asynchronously updated views, 0L is also a reasonable value.
+   * @param axis the axis this scroller can scroll
+   * @param startFromBeginning if {@code true},
+   *        {@link #scrollTo(DroidDriver, Finder, Finder)} starts from the
+   *        beginning and scrolls forward, instead of starting from the current
+   *        location and scrolling in both directions. It may not always work,
+   *        but when it works, it is faster.
+   */
+  public StepBasedScroller(int maxScrolls, long perScrollTimeoutMillis, Axis axis,
+      ScrollStepStrategy scrollStepStrategy, boolean startFromBeginning) {
+    this.maxScrolls = maxScrolls;
+    this.perScrollTimeoutMillis = perScrollTimeoutMillis;
+    this.axis = axis;
+    this.scrollStepStrategy = scrollStepStrategy;
+    this.startFromBeginning = startFromBeginning;
+  }
+
+  /**
+   * Constructs with default 100 maxScrolls, 1 second for
+   * perScrollTimeoutMillis, vertical axis, not startFromBegining.
+   */
+  public StepBasedScroller(ScrollStepStrategy scrollStepStrategy) {
+    this(100, 1000L, Axis.VERTICAL, scrollStepStrategy, false);
+  }
+
+  // if scrollBack is true, scrolls back to starting location if not found, so
+  // that we can start search in the other direction w/o polling on pages we
+  // have tried.
+  protected UiElement scrollTo(DroidDriver driver, Finder containerFinder, Finder itemFinder,
+      PhysicalDirection direction, boolean scrollBack) {
+    Logs.call(this, "scrollTo", driver, containerFinder, itemFinder, direction, scrollBack);
+    // Enforce itemFinder is relative to containerFinder.
+    // Combine with containerFinder to make itemFinder absolute.
+    itemFinder = By.chain(containerFinder, itemFinder);
+
+    int i = 0;
+    for (; i <= maxScrolls; i++) {
+      try {
+        return driver.getPoller()
+            .pollFor(driver, itemFinder, Poller.EXISTS, perScrollTimeoutMillis);
+      } catch (TimeoutException e) {
+        if (i < maxScrolls && !scrollStepStrategy.scroll(driver, containerFinder, direction)) {
+          break;
+        }
+      }
+    }
+
+    ElementNotFoundException exception = new ElementNotFoundException(itemFinder);
+    if (i == maxScrolls) {
+      // This is often a program error -- maxScrolls is a safety net; we should
+      // have either found itemFinder, or stopped scrolling b/c of reaching the
+      // end. If maxScrolls is reasonably large, ScrollStepStrategy must be
+      // wrong.
+      Logs.logfmt(Log.WARN, exception, "Scrolled %s %d times; ScrollStepStrategy=%s",
+          containerFinder, maxScrolls, scrollStepStrategy);
+    }
+
+    if (scrollBack) {
+      for (; i > 1; i--) {
+        driver.on(containerFinder).scroll(direction.reverse());
+      }
+    }
+    throw exception;
+  }
+
+  @Override
+  public UiElement scrollTo(DroidDriver driver, Finder containerFinder, Finder itemFinder,
+      PhysicalDirection direction) {
+    try {
+      scrollStepStrategy.beginScrolling(driver, containerFinder, itemFinder, direction);
+      return scrollTo(driver, containerFinder, itemFinder, direction, false);
+    } finally {
+      scrollStepStrategy.endScrolling(driver, containerFinder, itemFinder, direction);
+    }
+  }
+
+  @Override
+  public UiElement scrollTo(DroidDriver driver, Finder containerFinder, Finder itemFinder) {
+    Logs.call(this, "scrollTo", driver, containerFinder, itemFinder);
+    DirectionConverter converter = scrollStepStrategy.getDirectionConverter();
+    PhysicalDirection backwardDirection = converter.toPhysicalDirection(axis, BACKWARD);
+
+    if (startFromBeginning) {
+      // First try w/o scrolling
+      try {
+        return driver.getPoller().pollFor(driver, By.chain(containerFinder, itemFinder),
+            Poller.EXISTS, perScrollTimeoutMillis);
+      } catch (TimeoutException unused) {
+        // fall through to scroll to find
+      }
+
+      // Fling to beginning is not reliable; scroll to beginning
+      // container.perform(SwipeAction.toFling(backwardDirection));
+      try {
+        scrollStepStrategy.beginScrolling(driver, containerFinder, itemFinder, backwardDirection);
+        for (int i = 0; i < maxScrolls; i++) {
+          if (!scrollStepStrategy.scroll(driver, containerFinder, backwardDirection)) {
+            break;
+          }
+        }
+      } finally {
+        scrollStepStrategy.endScrolling(driver, containerFinder, itemFinder, backwardDirection);
+      }
+    } else {
+      // search backward first
+      try {
+        return scrollTo(driver, containerFinder, itemFinder, backwardDirection, true);
+      } catch (ElementNotFoundException e) {
+        // fall through to search forward
+      }
+    }
+
+    // search forward
+    return scrollTo(driver, containerFinder, itemFinder, backwardDirection.reverse(), false);
+  }
+}
diff --git a/src/com/google/android/droiddriver/uiautomation/AccessibilityDriver.java b/src/com/google/android/droiddriver/uiautomation/AccessibilityDriver.java
new file mode 100644
index 0000000..6e7441d
--- /dev/null
+++ b/src/com/google/android/droiddriver/uiautomation/AccessibilityDriver.java
@@ -0,0 +1,63 @@
+/*
+ * 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.uiautomation;
+
+import android.app.Instrumentation;
+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 Validator validator = new FirstApplicableValidator(new ExemptRootValidator(),
+      new ExemptScrollActionValidator(), new ExemptedClassesValidator(),
+      // TODO: ImageViewValidator
+      new DefaultAccessibilityValidator());
+
+  public AccessibilityDriver(Instrumentation instrumentation) {
+    super(instrumentation);
+  }
+
+  @Override
+  protected UiAutomationElement newUiElement(AccessibilityNodeInfo rawElement,
+      UiAutomationElement parent) {
+    UiAutomationElement newUiElement = super.newUiElement(rawElement, parent);
+    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/uiautomation/UiAutomationContext.java b/src/com/google/android/droiddriver/uiautomation/UiAutomationContext.java
index 7efa48e..253d4e5 100644
--- a/src/com/google/android/droiddriver/uiautomation/UiAutomationContext.java
+++ b/src/com/google/android/droiddriver/uiautomation/UiAutomationContext.java
@@ -16,50 +16,41 @@
 
 package com.google.android.droiddriver.uiautomation;
 
+import android.app.Instrumentation;
 import android.app.UiAutomation;
-import android.view.InputEvent;
 import android.view.accessibility.AccessibilityNodeInfo;
 
-import com.google.android.droiddriver.InputInjector;
-import com.google.android.droiddriver.base.AbstractContext;
-import com.google.common.collect.MapMaker;
+import com.google.android.droiddriver.base.DroidDriverContext;
+import com.google.android.droiddriver.exceptions.UnrecoverableException;
 
-import java.util.Map;
-
-/**
- * Internal helper for managing all instances.
- */
-public class UiAutomationContext extends AbstractContext {
-  // Maybe we should use Cache instead of Map on memory-constrained devices
-  private final Map<AccessibilityNodeInfo, UiAutomationElement> map = new MapMaker().weakKeys()
-      .weakValues().makeMap();
+public class UiAutomationContext extends
+    DroidDriverContext<AccessibilityNodeInfo, UiAutomationElement> {
   private final UiAutomation uiAutomation;
 
-  UiAutomationContext(final UiAutomation uiAutomation) {
-    super(new InputInjector() {
-      @Override
-      public boolean injectInputEvent(InputEvent event) {
-        return uiAutomation.injectInputEvent(event, true /* sync */);
-      }
-    });
-    this.uiAutomation = uiAutomation;
-  }
-
-  public UiAutomationElement getUiElement(AccessibilityNodeInfo node) {
-    UiAutomationElement element = map.get(node);
-    if (element == null) {
-      element = new UiAutomationElement(this, node);
-      map.put(node, element);
-    }
-    return element;
+  public UiAutomationContext(Instrumentation instrumentation, UiAutomationDriver driver) {
+    super(instrumentation, driver);
+    this.uiAutomation = instrumentation.getUiAutomation();
   }
 
   @Override
-  public void clearData() {
-    map.clear();
+  public UiAutomationDriver getDriver() {
+    return (UiAutomationDriver) super.getDriver();
   }
 
-  public UiAutomation getUiAutomation() {
-    return uiAutomation;
+  public interface UiAutomationCallable<T> {
+    T call(UiAutomation uiAutomation);
+  }
+
+  /**
+   * Wraps calls to UiAutomation API. Currently supports fail-fast if
+   * UiAutomation throws IllegalStateException, which occurs when the connection
+   * to UiAutomation service is lost.
+   */
+  public <T> T callUiAutomation(UiAutomationCallable<T> uiAutomationCallable) {
+    try {
+      return uiAutomationCallable.call(uiAutomation);
+    } catch (IllegalStateException e) {
+      throw new UnrecoverableException(e);
+    }
   }
 }
diff --git a/src/com/google/android/droiddriver/uiautomation/UiAutomationDriver.java b/src/com/google/android/droiddriver/uiautomation/UiAutomationDriver.java
index 7d21233..f139a15 100644
--- a/src/com/google/android/droiddriver/uiautomation/UiAutomationDriver.java
+++ b/src/com/google/android/droiddriver/uiautomation/UiAutomationDriver.java
@@ -16,20 +16,25 @@
 
 package com.google.android.droiddriver.uiautomation;
 
-import android.app.UiAutomation;
 import android.app.Instrumentation;
-import android.graphics.Bitmap;
+import android.app.UiAutomation;
+import android.content.Context;
 import android.os.SystemClock;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
 import android.view.accessibility.AccessibilityNodeInfo;
 
-import com.google.android.droiddriver.base.AbstractDroidDriver;
+import com.google.android.droiddriver.actions.InputInjector;
+import com.google.android.droiddriver.base.BaseDroidDriver;
 import com.google.android.droiddriver.exceptions.TimeoutException;
-import com.google.common.primitives.Longs;
+import com.google.android.droiddriver.uiautomation.UiAutomationContext.UiAutomationCallable;
+import com.google.android.droiddriver.util.Logs;
 
 /**
- * Implementation of a DroidDriver that is driven via the accessibility layer.
+ * Implementation of DroidDriver that gets attributes via the Accessibility API
+ * and is acted upon via synthesized events.
  */
-public class UiAutomationDriver extends AbstractDroidDriver {
+public class UiAutomationDriver extends BaseDroidDriver<AccessibilityNodeInfo, UiAutomationElement> {
   // TODO: magic const from UiAutomator, but may not be useful
   /**
    * This value has the greatest bearing on the appearance of test execution
@@ -39,34 +44,56 @@
   private static final long QUIET_TIME_TO_BE_CONSIDERD_IDLE_STATE = 500;// ms
 
   private final UiAutomationContext context;
-  private final UiAutomation uiAutomation;
+  private final InputInjector injector;
+  private final UiAutomationUiDevice uiDevice;
+  private AccessibilityNodeInfoCacheClearer clearer =
+      new WindowStateAccessibilityNodeInfoCacheClearer();
 
   public UiAutomationDriver(Instrumentation instrumentation) {
-    super(instrumentation);
-    this.uiAutomation = instrumentation.getUiAutomation();
-    this.context = new UiAutomationContext(uiAutomation);
+    context = new UiAutomationContext(instrumentation, this);
+    injector = new UiAutomationInputInjector(context);
+    uiDevice = new UiAutomationUiDevice(context);
   }
 
   @Override
-  protected UiAutomationElement getNewRootElement() {
-    return context.getUiElement(getRootNode());
+  public InputInjector getInjector() {
+    return injector;
   }
 
   @Override
-  protected UiAutomationContext getContext() {
-    return context;
+  protected UiAutomationElement newRootElement() {
+    return context.newRootElement(getRootNode());
+  }
+
+  @Override
+  protected UiAutomationElement newUiElement(AccessibilityNodeInfo rawElement,
+      UiAutomationElement parent) {
+    return new UiAutomationElement(context, rawElement, parent);
   }
 
   private AccessibilityNodeInfo getRootNode() {
-    long timeoutMillis = getPoller().getTimeoutMillis();
-    try {
-      uiAutomation.waitForIdle(QUIET_TIME_TO_BE_CONSIDERD_IDLE_STATE, timeoutMillis);
-    } catch (java.util.concurrent.TimeoutException e) {
-      throw new TimeoutException(e);
-    }
+    final long timeoutMillis = getPoller().getTimeoutMillis();
+    context.callUiAutomation(new UiAutomationCallable<Void>() {
+      @Override
+      public Void call(UiAutomation uiAutomation) {
+        try {
+          uiAutomation.waitForIdle(QUIET_TIME_TO_BE_CONSIDERD_IDLE_STATE, timeoutMillis);
+          return null;
+        } catch (java.util.concurrent.TimeoutException e) {
+          throw new TimeoutException(e);
+        }
+      }
+    });
+
     long end = SystemClock.uptimeMillis() + timeoutMillis;
     while (true) {
-      AccessibilityNodeInfo root = uiAutomation.getRootInActiveWindow();
+      AccessibilityNodeInfo root =
+          context.callUiAutomation(new UiAutomationCallable<AccessibilityNodeInfo>() {
+            @Override
+            public AccessibilityNodeInfo call(UiAutomation uiAutomation) {
+              return uiAutomation.getRootInActiveWindow();
+            }
+          });
       if (root != null) {
         return root;
       }
@@ -76,12 +103,56 @@
             String.format("Timed out after %d milliseconds waiting for root AccessibilityNodeInfo",
                 timeoutMillis));
       }
-      SystemClock.sleep(Longs.min(250, remainingMillis));
+      SystemClock.sleep(Math.min(250, remainingMillis));
     }
   }
 
+  /**
+   * Some widgets fail to trigger some AccessibilityEvent's after actions,
+   * resulting in stale AccessibilityNodeInfo's. As a work-around, force to
+   * clear the AccessibilityNodeInfoCache.
+   */
+  public void clearAccessibilityNodeInfoCache() {
+    Logs.call(this, "clearAccessibilityNodeInfoCache");
+    clearer.clearAccessibilityNodeInfoCache(this);
+  }
+
+  public interface AccessibilityNodeInfoCacheClearer {
+    void clearAccessibilityNodeInfoCache(UiAutomationDriver driver);
+  }
+
+  /**
+   * Clears AccessibilityNodeInfoCache by turning screen off then on.
+   */
+  public static class ScreenOffAccessibilityNodeInfoCacheClearer implements
+      AccessibilityNodeInfoCacheClearer {
+    public void clearAccessibilityNodeInfoCache(UiAutomationDriver driver) {
+      driver.getUiDevice().sleep();
+      driver.getUiDevice().wakeUp();
+    }
+  }
+
+  /**
+   * Clears AccessibilityNodeInfoCache by exploiting an implementation detail of
+   * AccessibilityNodeInfoCache. This is a hack; use it at your own discretion.
+   */
+  public static class WindowStateAccessibilityNodeInfoCacheClearer implements
+      AccessibilityNodeInfoCacheClearer {
+    public void clearAccessibilityNodeInfoCache(UiAutomationDriver driver) {
+      AccessibilityManager accessibilityManager =
+          (AccessibilityManager) driver.context.getInstrumentation().getTargetContext()
+              .getSystemService(Context.ACCESSIBILITY_SERVICE);
+      accessibilityManager.sendAccessibilityEvent(AccessibilityEvent
+          .obtain(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED));
+    }
+  }
+
+  public void setAccessibilityNodeInfoCacheClearer(AccessibilityNodeInfoCacheClearer clearer) {
+    this.clearer = clearer;
+  }
+
   @Override
-  protected Bitmap takeScreenshot() {
-    return uiAutomation.takeScreenshot();
+  public UiAutomationUiDevice getUiDevice() {
+    return uiDevice;
   }
 }
diff --git a/src/com/google/android/droiddriver/uiautomation/UiAutomationElement.java b/src/com/google/android/droiddriver/uiautomation/UiAutomationElement.java
index a399360..e737bc6 100644
--- a/src/com/google/android/droiddriver/uiautomation/UiAutomationElement.java
+++ b/src/com/google/android/droiddriver/uiautomation/UiAutomationElement.java
@@ -16,27 +16,32 @@
 
 package com.google.android.droiddriver.uiautomation;
 
-import static com.google.android.droiddriver.util.TextUtils.charSequenceToString;
+import static com.google.android.droiddriver.util.Strings.charSequenceToString;
 
 import android.app.UiAutomation;
 import android.app.UiAutomation.AccessibilityEventFilter;
 import android.graphics.Rect;
-import android.util.Log;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityNodeInfo;
 
-import com.google.android.droiddriver.InputInjector;
-import com.google.android.droiddriver.base.AbstractUiElement;
-import com.google.android.droiddriver.util.Logs;
-import com.google.common.base.Preconditions;
+import com.google.android.droiddriver.actions.InputInjector;
+import com.google.android.droiddriver.base.BaseUiElement;
+import com.google.android.droiddriver.finders.Attribute;
+import com.google.android.droiddriver.uiautomation.UiAutomationContext.UiAutomationCallable;
+import com.google.android.droiddriver.util.Preconditions;
 
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
 import java.util.concurrent.FutureTask;
 import java.util.concurrent.TimeoutException;
 
 /**
- * A UiElement that is backed by the UiAutomation object.
+ * A UiElement that gets attributes via the Accessibility API.
  */
-public class UiAutomationElement extends AbstractUiElement {
+public class UiAutomationElement extends BaseUiElement<AccessibilityNodeInfo, UiAutomationElement> {
   private static final AccessibilityEventFilter ANY_EVENT_FILTER = new AccessibilityEventFilter() {
     @Override
     public boolean accept(AccessibilityEvent arg0) {
@@ -44,112 +49,89 @@
     }
   };
 
-  private final UiAutomationContext context;
   private final AccessibilityNodeInfo node;
-  private final UiAutomation uiAutomation;
+  private final UiAutomationContext context;
+  private final Map<Attribute, Object> attributes;
+  private final boolean visible;
+  private final Rect visibleBounds;
+  private final UiAutomationElement parent;
+  private final List<UiAutomationElement> children;
 
-  public UiAutomationElement(UiAutomationContext context, AccessibilityNodeInfo node) {
-    this.context = Preconditions.checkNotNull(context);
+  /**
+   * A snapshot of all attributes is taken at construction. The attributes of a
+   * {@code UiAutomationElement} instance are immutable. If the underlying
+   * {@link AccessibilityNodeInfo} is updated, a new {@code UiAutomationElement}
+   * instance will be created in
+   * {@link com.google.android.droiddriver.DroidDriver#refreshUiElementTree}.
+   */
+  protected UiAutomationElement(UiAutomationContext context, AccessibilityNodeInfo node,
+      UiAutomationElement parent) {
     this.node = Preconditions.checkNotNull(node);
-    this.uiAutomation = context.getUiAutomation();
+    this.context = Preconditions.checkNotNull(context);
+    this.parent = parent;
+
+    Map<Attribute, Object> attribs = new EnumMap<Attribute, Object>(Attribute.class);
+    put(attribs, Attribute.PACKAGE, charSequenceToString(node.getPackageName()));
+    put(attribs, Attribute.CLASS, charSequenceToString(node.getClassName()));
+    put(attribs, Attribute.TEXT, charSequenceToString(node.getText()));
+    put(attribs, Attribute.CONTENT_DESC, charSequenceToString(node.getContentDescription()));
+    put(attribs, Attribute.RESOURCE_ID, charSequenceToString(node.getViewIdResourceName()));
+    put(attribs, Attribute.CHECKABLE, node.isCheckable());
+    put(attribs, Attribute.CHECKED, node.isChecked());
+    put(attribs, Attribute.CLICKABLE, node.isClickable());
+    put(attribs, Attribute.ENABLED, node.isEnabled());
+    put(attribs, Attribute.FOCUSABLE, node.isFocusable());
+    put(attribs, Attribute.FOCUSED, node.isFocused());
+    put(attribs, Attribute.LONG_CLICKABLE, node.isLongClickable());
+    put(attribs, Attribute.PASSWORD, node.isPassword());
+    put(attribs, Attribute.SCROLLABLE, node.isScrollable());
+    if (node.getTextSelectionStart() >= 0
+        && node.getTextSelectionStart() != node.getTextSelectionEnd()) {
+      attribs.put(Attribute.SELECTION_START, node.getTextSelectionStart());
+      attribs.put(Attribute.SELECTION_END, node.getTextSelectionEnd());
+    }
+    put(attribs, Attribute.SELECTED, node.isSelected());
+    put(attribs, Attribute.BOUNDS, getBounds(node));
+    attributes = Collections.unmodifiableMap(attribs);
+
+    // Order matters as getVisibleBounds depends on visible
+    visible = node.isVisibleToUser();
+    visibleBounds = getVisibleBounds(node);
+    List<UiAutomationElement> mutableChildren = buildChildren(node);
+    this.children = mutableChildren == null ? null : Collections.unmodifiableList(mutableChildren);
   }
 
-  @Override
-  public String getText() {
-    return charSequenceToString(node.getText());
+  private void put(Map<Attribute, Object> attribs, Attribute key, Object value) {
+    if (value != null) {
+      attribs.put(key, value);
+    }
   }
 
-  @Override
-  public String getContentDescription() {
-    return charSequenceToString(node.getContentDescription());
+  private List<UiAutomationElement> buildChildren(AccessibilityNodeInfo node) {
+    List<UiAutomationElement> children;
+    int childCount = node.getChildCount();
+    if (childCount == 0) {
+      children = null;
+    } else {
+      children = new ArrayList<UiAutomationElement>(childCount);
+      for (int i = 0; i < childCount; i++) {
+        AccessibilityNodeInfo child = node.getChild(i);
+        if (child != null) {
+          children.add(context.getElement(child, this));
+        }
+      }
+    }
+    return children;
   }
 
-  @Override
-  public String getClassName() {
-    return charSequenceToString(node.getClassName());
-  }
-
-  @Override
-  public String getResourceId() {
-    return charSequenceToString(node.getViewIdResourceName());
-  }
-
-  @Override
-  public String getPackageName() {
-    return charSequenceToString(node.getPackageName());
-  }
-
-  @Override
-  public InputInjector getInjector() {
-    return context.getInjector();
-  }
-
-  @Override
-  public boolean isVisible() {
-    return node.isVisibleToUser();
-  }
-
-  @Override
-  public boolean isCheckable() {
-    return node.isCheckable();
-  }
-
-  @Override
-  public boolean isChecked() {
-    return node.isChecked();
-  }
-
-  @Override
-  public boolean isClickable() {
-    return node.isClickable();
-  }
-
-  @Override
-  public boolean isEnabled() {
-    return node.isEnabled();
-  }
-
-  @Override
-  public boolean isFocusable() {
-    return node.isFocusable();
-  }
-
-  @Override
-  public boolean isFocused() {
-    return node.isFocused();
-  }
-
-  @Override
-  public boolean isScrollable() {
-    return node.isScrollable();
-  }
-
-  @Override
-  public boolean isLongClickable() {
-    return node.isLongClickable();
-  }
-
-  @Override
-  public boolean isPassword() {
-    return node.isPassword();
-  }
-
-  @Override
-  public boolean isSelected() {
-    return node.isSelected();
-  }
-
-  @Override
-  public Rect getBounds() {
+  private Rect getBounds(AccessibilityNodeInfo node) {
     Rect rect = new Rect();
     node.getBoundsInScreen(rect);
     return rect;
   }
 
-  @Override
-  public Rect getVisibleBounds() {
-    if (!isVisible()) {
-      Logs.log(Log.DEBUG, "Node is invisible: " + node);
+  private Rect getVisibleBounds(AccessibilityNodeInfo node) {
+    if (!visible) {
       return new Rect();
     }
     Rect visibleBounds = getBounds();
@@ -164,31 +146,68 @@
   }
 
   @Override
-  public int getChildCount() {
-    return node.getChildCount();
+  public Rect getVisibleBounds() {
+    return visibleBounds;
   }
 
   @Override
-  public UiAutomationElement getChild(int index) {
-    AccessibilityNodeInfo child = node.getChild(index);
-    return child == null ? null : context.getUiElement(child);
+  public boolean isVisible() {
+    return visible;
   }
 
   @Override
   public UiAutomationElement getParent() {
-    AccessibilityNodeInfo parent = node.getParent();
-    return parent == null ? null : context.getUiElement(parent);
+    return parent;
   }
 
   @Override
-  protected void doPerformAndWait(FutureTask<Boolean> futureTask, long timeoutMillis) {
-    try {
-      uiAutomation.executeAndWaitForEvent(futureTask, ANY_EVENT_FILTER, timeoutMillis);
-    } catch (TimeoutException e) {
-      // This is for sync'ing with Accessibility API on best-effort because
-      // it is not reliable.
-      // Exception is ignored here. Tests will fail anyways if this is
-      // critical.
-    }
+  protected List<UiAutomationElement> getChildren() {
+    return children;
+  }
+
+  @Override
+  protected Map<Attribute, Object> getAttributes() {
+    return attributes;
+  }
+
+  @Override
+  public InputInjector getInjector() {
+    return context.getDriver().getInjector();
+  }
+
+  /**
+   * Note: This implementation of {@code doPerformAndWait} clears the
+   * {@code AccessibilityEvent} queue.
+   */
+  @Override
+  protected void doPerformAndWait(final FutureTask<Boolean> futureTask, final long timeoutMillis) {
+    context.callUiAutomation(new UiAutomationCallable<Void>() {
+
+      @Override
+      public Void call(UiAutomation uiAutomation) {
+        try {
+          uiAutomation.executeAndWaitForEvent(futureTask, ANY_EVENT_FILTER, timeoutMillis);
+        } catch (TimeoutException e) {
+          // This is for sync'ing with Accessibility API on best-effort because
+          // it is not reliable.
+          // Exception is ignored here. Tests will fail anyways if this is
+          // critical.
+          // Actions should usually trigger some AccessibilityEvent's, but some
+          // widgets fail to do so, resulting in stale AccessibilityNodeInfo's.
+          // As a work-around, force to clear the AccessibilityNodeInfoCache.
+          // A legitimate case of no AccessibilityEvent is when scrolling has
+          // reached the end, but we cannot tell whether it's legitimate or the
+          // widget has bugs, so clearAccessibilityNodeInfoCache anyways.
+          context.getDriver().clearAccessibilityNodeInfoCache();
+        }
+        return null;
+      }
+
+    });
+  }
+
+  @Override
+  public AccessibilityNodeInfo getRawElement() {
+    return node;
   }
 }
diff --git a/src/com/google/android/droiddriver/uiautomation/UiAutomationInputInjector.java b/src/com/google/android/droiddriver/uiautomation/UiAutomationInputInjector.java
new file mode 100644
index 0000000..94d3ab4
--- /dev/null
+++ b/src/com/google/android/droiddriver/uiautomation/UiAutomationInputInjector.java
@@ -0,0 +1,41 @@
+/*
+ * 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.uiautomation;
+
+import android.app.UiAutomation;
+import android.view.InputEvent;
+
+import com.google.android.droiddriver.actions.InputInjector;
+import com.google.android.droiddriver.uiautomation.UiAutomationContext.UiAutomationCallable;
+
+public class UiAutomationInputInjector implements InputInjector {
+  private final UiAutomationContext context;
+
+  public UiAutomationInputInjector(UiAutomationContext context) {
+    this.context = context;
+  }
+
+  @Override
+  public boolean injectInputEvent(final InputEvent event) {
+    return context.callUiAutomation(new UiAutomationCallable<Boolean>() {
+      @Override
+      public Boolean call(UiAutomation uiAutomation) {
+        return uiAutomation.injectInputEvent(event, true /* sync */);
+      }
+    });
+  }
+}
diff --git a/src/com/google/android/droiddriver/uiautomation/UiAutomationUiDevice.java b/src/com/google/android/droiddriver/uiautomation/UiAutomationUiDevice.java
new file mode 100644
index 0000000..a376cb6
--- /dev/null
+++ b/src/com/google/android/droiddriver/uiautomation/UiAutomationUiDevice.java
@@ -0,0 +1,56 @@
+/*
+ * 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.uiautomation;
+
+import android.app.UiAutomation;
+import android.graphics.Bitmap;
+import android.util.Log;
+
+import com.google.android.droiddriver.base.BaseUiDevice;
+import com.google.android.droiddriver.exceptions.UnrecoverableException;
+import com.google.android.droiddriver.uiautomation.UiAutomationContext.UiAutomationCallable;
+import com.google.android.droiddriver.util.Logs;
+
+class UiAutomationUiDevice extends BaseUiDevice {
+  private final UiAutomationContext context;
+
+  UiAutomationUiDevice(UiAutomationContext context) {
+    this.context = context;
+  }
+
+  @Override
+  protected Bitmap takeScreenshot() {
+    try {
+      return context.callUiAutomation(new UiAutomationCallable<Bitmap>() {
+        @Override
+        public Bitmap call(UiAutomation uiAutomation) {
+          return uiAutomation.takeScreenshot();
+        }
+      });
+    } catch (UnrecoverableException e) {
+      throw e;
+    } catch (Throwable e) {
+      Logs.log(Log.ERROR, e);
+      return null;
+    }
+  }
+
+  @Override
+  protected UiAutomationContext getContext() {
+    return context;
+  }
+}
diff --git a/src/com/google/android/droiddriver/util/ActivityUtils.java b/src/com/google/android/droiddriver/util/ActivityUtils.java
index 466caf9..c0f553f 100644
--- a/src/com/google/android/droiddriver/util/ActivityUtils.java
+++ b/src/com/google/android/droiddriver/util/ActivityUtils.java
@@ -19,12 +19,21 @@
 import android.app.Activity;
 
 import com.google.android.droiddriver.instrumentation.InstrumentationDriver;
-import com.google.common.base.Supplier;
 
 /**
  * Static helper methods for retrieving activities.
  */
 public class ActivityUtils {
+  public interface Supplier<T> {
+    /**
+     * Retrieves an instance of the appropriate type. The returned object may or
+     * may not be a new instance, depending on the implementation.
+     *
+     * @return an instance of the appropriate type
+     */
+    T get();
+  }
+
   private static Supplier<Activity> runningActivitySupplier;
 
   /**
diff --git a/src/com/google/android/droiddriver/util/Events.java b/src/com/google/android/droiddriver/util/Events.java
index 14cb90e..5268780 100644
--- a/src/com/google/android/droiddriver/util/Events.java
+++ b/src/com/google/android/droiddriver/util/Events.java
@@ -21,7 +21,7 @@
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 
-import com.google.android.droiddriver.InputInjector;
+import com.google.android.droiddriver.actions.InputInjector;
 import com.google.android.droiddriver.exceptions.ActionException;
 
 /**
diff --git a/src/com/google/android/droiddriver/util/FileUtils.java b/src/com/google/android/droiddriver/util/FileUtils.java
index 5ffe681..07abb5a 100644
--- a/src/com/google/android/droiddriver/util/FileUtils.java
+++ b/src/com/google/android/droiddriver/util/FileUtils.java
@@ -31,7 +31,8 @@
 public class FileUtils {
   /**
    * Opens file at {@code path} to output. If any directories on {@code path} do
-   * not exist, they will be created. The file will be readable to all.
+   * not exist, they will be created. The file will be readable and writable to
+   * all.
    */
   public static BufferedOutputStream open(String path) throws FileNotFoundException {
     File file = getAbsoluteFile(path);
@@ -39,6 +40,7 @@
     Logs.log(Log.INFO, "opening file " + file.getAbsolutePath());
     BufferedOutputStream stream = new BufferedOutputStream(new FileOutputStream(file));
     file.setReadable(true /* readable */, false/* ownerOnly */);
+    file.setWritable(true /* readable */, false/* ownerOnly */);
     return stream;
   }
 
@@ -68,6 +70,7 @@
       throw new DroidDriverException("failed to mkdir " + dir);
     }
     dir.setReadable(true /* readable */, false/* ownerOnly */);
+    dir.setWritable(true /* readable */, false/* ownerOnly */);
     dir.setExecutable(true /* executable */, false/* ownerOnly */);
   }
 }
diff --git a/src/com/google/android/droiddriver/util/Logs.java b/src/com/google/android/droiddriver/util/Logs.java
index 5287ccd..e08ee10 100644
--- a/src/com/google/android/droiddriver/util/Logs.java
+++ b/src/com/google/android/droiddriver/util/Logs.java
@@ -16,10 +16,9 @@
 
 package com.google.android.droiddriver.util;
 
+import android.text.TextUtils;
 import android.util.Log;
 
-import com.google.common.base.Joiner;
-
 /**
  * Internal helper for logging.
  */
@@ -32,7 +31,7 @@
       Log.d(
           TAG,
           String.format("Invoking %s.%s(%s)", self.getClass().getSimpleName(), method,
-              Joiner.on(",").join(args)));
+              TextUtils.join(", ", args)));
     }
   }
 
diff --git a/src/com/google/android/droiddriver/util/Preconditions.java b/src/com/google/android/droiddriver/util/Preconditions.java
new file mode 100644
index 0000000..aaf545d
--- /dev/null
+++ b/src/com/google/android/droiddriver/util/Preconditions.java
@@ -0,0 +1,40 @@
+/*
+ * 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.util;
+
+/**
+ * Simple static methods to be called at the start of your own methods to verify
+ * correct arguments and state.
+ */
+public final class Preconditions {
+  private Preconditions() {}
+
+  /**
+   * Ensures that an object reference passed as a parameter to the calling
+   * method is not null.
+   *
+   * @param reference an object reference
+   * @return the non-null reference that was validated
+   * @throws NullPointerException if {@code reference} is null
+   */
+  public static <T> T checkNotNull(T reference) {
+    if (reference == null) {
+      throw new NullPointerException();
+    }
+    return reference;
+  }
+}
diff --git a/src/com/google/android/droiddriver/util/Strings.java b/src/com/google/android/droiddriver/util/Strings.java
new file mode 100644
index 0000000..ea75238
--- /dev/null
+++ b/src/com/google/android/droiddriver/util/Strings.java
@@ -0,0 +1,66 @@
+/*
+ * 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.util;
+
+/**
+ * Static helper methods for manipulating strings.
+ */
+public class Strings {
+  public static String charSequenceToString(CharSequence input) {
+    return input == null ? null : input.toString();
+  }
+
+  public static ToStringHelper toStringHelper(Object self) {
+    return new ToStringHelper(self.getClass().getSimpleName());
+  }
+
+  public static final class ToStringHelper {
+    private final StringBuilder builder;
+    private boolean needsSeparator = false;
+
+    /**
+     * Use {@link #toStringHelper(Object)} to create an instance.
+     */
+    private ToStringHelper(String className) {
+      this.builder = new StringBuilder(32).append(className).append('{');
+    }
+
+    public ToStringHelper addValue(Object value) {
+      maybeAppendSeparator().append(value);
+      return this;
+    }
+
+    public ToStringHelper add(String name, Object value) {
+      maybeAppendSeparator().append(name).append('=').append(value);
+      return this;
+    }
+
+    @Override
+    public String toString() {
+      return builder.append('}').toString();
+    }
+
+    private StringBuilder maybeAppendSeparator() {
+      if (needsSeparator) {
+        return builder.append(", ");
+      } else {
+        needsSeparator = true;
+        return builder;
+      }
+    }
+  }
+}
diff --git a/src/com/google/android/droiddriver/util/TextUtils.java b/src/com/google/android/droiddriver/util/TextUtils.java
deleted file mode 100644
index b709018..0000000
--- a/src/com/google/android/droiddriver/util/TextUtils.java
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * 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.util;
-
-/**
- * Static helper methods for manipulating strings.
- */
-public class TextUtils {
-  public static String charSequenceToString(CharSequence input) {
-    return input == null ? null : input.toString();
-  }
-}
diff --git a/src/com/google/android/droiddriver/validators/DefaultAccessibilityValidator.java b/src/com/google/android/droiddriver/validators/DefaultAccessibilityValidator.java
new file mode 100644
index 0000000..5f49bcd
--- /dev/null
+++ b/src/com/google/android/droiddriver/validators/DefaultAccessibilityValidator.java
@@ -0,0 +1,88 @@
+/*
+ * 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.text.TextUtils;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import com.google.android.droiddriver.UiElement;
+import com.google.android.droiddriver.actions.Action;
+import com.google.android.droiddriver.uiautomation.UiAutomationElement;
+
+/**
+ * Fall-back Validator for accessibility.
+ */
+public class DefaultAccessibilityValidator implements Validator {
+  @Override
+  public boolean isApplicable(UiElement element, Action action) {
+    return true;
+  }
+
+  @Override
+  public String validate(UiElement element, Action action) {
+    return isSpeakingNode(element) ? null : "TalkBack cannot speak about it";
+  }
+
+  // Logic from TalkBack
+  private static boolean isAccessibilityFocusable(UiElement element) {
+    if (isActionableForAccessibility(element)) {
+      return true;
+    }
+
+    if (isTopLevelScrollItem(element) && (isSpeakingNode(element))) {
+      return true;
+    }
+    return false;
+  }
+
+  private static boolean isTopLevelScrollItem(UiElement element) {
+    UiElement parent = element.getParent();
+    return parent != null && parent.isScrollable();
+  }
+
+  private static boolean isActionableForAccessibility(UiElement element) {
+    if (element.isFocusable() || element.isClickable() || element.isLongClickable()) {
+      return true;
+    }
+
+    if (element instanceof UiAutomationElement) {
+      AccessibilityNodeInfo node = ((UiAutomationElement) element).getRawElement();
+      return (node.getActions() & AccessibilityNodeInfo.ACTION_FOCUS) == AccessibilityNodeInfo.ACTION_FOCUS;
+    }
+    return false;
+  }
+
+  private static boolean isSpeakingNode(UiElement element) {
+    return hasContentDescriptionOrText(element) || element.isCheckable()
+        || hasNonActionableSpeakingChildren(element);
+  }
+
+  private static boolean hasNonActionableSpeakingChildren(UiElement element) {
+    // Recursively check visible and non-focusable descendant nodes.
+    for (UiElement child : element.getChildren(UiElement.VISIBLE)) {
+      if (!isAccessibilityFocusable(child) && isSpeakingNode(child)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private static boolean hasContentDescriptionOrText(UiElement element) {
+    return !TextUtils.isEmpty(element.getContentDescription())
+        || !TextUtils.isEmpty(element.getText());
+  }
+}
diff --git a/src/com/google/android/droiddriver/exceptions/ElementNotVisibleException.java b/src/com/google/android/droiddriver/validators/ExemptRootValidator.java
similarity index 60%
copy from src/com/google/android/droiddriver/exceptions/ElementNotVisibleException.java
copy to src/com/google/android/droiddriver/validators/ExemptRootValidator.java
index b3ac86e..1846e37 100644
--- a/src/com/google/android/droiddriver/exceptions/ElementNotVisibleException.java
+++ b/src/com/google/android/droiddriver/validators/ExemptRootValidator.java
@@ -14,17 +14,22 @@
  * limitations under the License.
  */
 
-package com.google.android.droiddriver.exceptions;
+package com.google.android.droiddriver.validators;
 
 import com.google.android.droiddriver.UiElement;
+import com.google.android.droiddriver.actions.Action;
 
 /**
- * Thrown when an element is not visible on screen, therefore cannot be
- * interacted with.
+ * Exempts root from validation.
  */
-@SuppressWarnings("serial")
-public class ElementNotVisibleException extends DroidDriverException {
-  public ElementNotVisibleException(UiElement element) {
-    super("Invisible on screen: " + element);
+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
new file mode 100644
index 0000000..4c0debb
--- /dev/null
+++ b/src/com/google/android/droiddriver/validators/Validator.java
@@ -0,0 +1,39 @@
+/*
+ * 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;
+
+/**
+ * Interface for validating a UiElement, checked when an action is performed.
+ * For example, in general accessibility mandates that an actionable UiElement
+ * has content description or text.
+ */
+public interface Validator {
+  /**
+   * Returns true if this {@link Validator} applies to {@code element} on this
+   * {@code action}.
+   */
+  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/exceptions/ElementNotVisibleException.java b/src/com/google/android/droiddriver/validators/VisibilityValidator.java
similarity index 61%
rename from src/com/google/android/droiddriver/exceptions/ElementNotVisibleException.java
rename to src/com/google/android/droiddriver/validators/VisibilityValidator.java
index b3ac86e..df19bd7 100644
--- a/src/com/google/android/droiddriver/exceptions/ElementNotVisibleException.java
+++ b/src/com/google/android/droiddriver/validators/VisibilityValidator.java
@@ -14,17 +14,22 @@
  * limitations under the License.
  */
 
-package com.google.android.droiddriver.exceptions;
+package com.google.android.droiddriver.validators;
 
 import com.google.android.droiddriver.UiElement;
+import com.google.android.droiddriver.actions.Action;
 
 /**
- * Thrown when an element is not visible on screen, therefore cannot be
- * interacted with.
+ * Validates visibility.
  */
-@SuppressWarnings("serial")
-public class ElementNotVisibleException extends DroidDriverException {
-  public ElementNotVisibleException(UiElement element) {
-    super("Invisible on screen: " + element);
+public class VisibilityValidator implements Validator {
+  @Override
+  public boolean isApplicable(UiElement element, Action action) {
+    return true;
+  }
+
+  @Override
+  public String validate(UiElement element, Action action) {
+    return element.isVisible() ? null : "invisible";
   }
 }
diff --git a/to-uiautomator.xsl b/to-uiautomator.xsl
index 7c68454..f7d46af 100644
--- a/to-uiautomator.xsl
+++ b/to-uiautomator.xsl
@@ -1,6 +1,6 @@
 <?xml version="1.0"?>
-<!-- To convert DroidDriver dump (say dd.xml) to UiAutomatorViewer format (say ua.uix), run:
-     xsltproc -o ua.uix to-uiautomator.xsl dd.xml -->
+<!-- To convert DroidDriver dump (say dd.xml) to UiAutomatorViewer format (say ua.uix), run: -->
+<!-- xsltproc -o ua.uix to-uiautomator.xsl dd.xml -->
 <xsl:transform version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
   <xsl:strip-space elements="*" />
   <xsl:template match="/">
@@ -26,7 +26,21 @@
       <xsl:attribute name="long-clickable"><xsl:value-of select="boolean(@long-clickable)" /></xsl:attribute>
       <xsl:attribute name="password"><xsl:value-of select="boolean(@password)" /></xsl:attribute>
       <xsl:attribute name="selected"><xsl:value-of select="boolean(@selected)" /></xsl:attribute>
+      <xsl:if test="@selection-start">
+        <xsl:attribute name="selection-start"><xsl:value-of select="@selection-start" /></xsl:attribute>
+      </xsl:if>
+      <xsl:if test="@selection-end">
+        <xsl:attribute name="selection-end"><xsl:value-of select="@selection-end" /></xsl:attribute>
+      </xsl:if>
       <xsl:attribute name="bounds"><xsl:value-of select="@bounds" /></xsl:attribute>
+      <!-- These two attributes are added by DroidDriver to help diagnosis -->
+      <!-- They are intentionally named in a different style -->
+      <xsl:if test="@NotVisible">
+        <xsl:attribute name="NotVisible">true</xsl:attribute>
+      </xsl:if>
+      <xsl:if test="@VisibleBounds">
+        <xsl:attribute name="VisibleBounds"><xsl:value-of select="@VisibleBounds" /></xsl:attribute>
+      </xsl:if>
       <xsl:apply-templates />
     </node>
   </xsl:template>