Merge "Merge GoogleInstrumentationTestRunner features into android.support.test."
diff --git a/espresso/build.gradle b/espresso/build.gradle
new file mode 100644
index 0000000..c2d71cb
--- /dev/null
+++ b/espresso/build.gradle
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * 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.
+ */
+
+buildscript {
+    repositories {
+        maven { url '../../../prebuilts/gradle-plugin' }
+        maven { url '../../../prebuilts/tools/common/m2/repository' }
+        maven { url '../../../prebuilts/tools/common/m2/internal' }
+    }
+    dependencies {
+        classpath 'com.android.tools.build:gradle:0.10.+'
+    }
+}
+
+subprojects {
+    project.ext {
+        androidSdkPath = getAndroidSdkPath()
+        println 'Using Android SDK at: ' + androidSdkPath
+    }
+}
+
+def getAndroidSdkPath() {
+    if (project.has("androidCustomSdkPath")) {
+        project.androidCustomSdkPath
+    } else {
+        System.getenv("ANDROID_HOME")
+    }
+}
diff --git a/espresso/espresso-contrib-tests/build.gradle b/espresso/espresso-contrib-tests/build.gradle
new file mode 100644
index 0000000..e41b8aa
--- /dev/null
+++ b/espresso/espresso-contrib-tests/build.gradle
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * 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.
+ */
+
+apply plugin: 'android'
+
+repositories {
+    maven { url '../../../../prebuilts/tools/common/m2/repository' }
+    maven { url '../../../../prebuilts/tools/common/m2/internal' }
+}
+
+android {
+    compileSdkVersion 19
+    buildToolsVersion "19.0.3"
+
+    packagingOptions {
+        exclude 'LICENSE.txt'
+    }
+
+    lintOptions {
+        abortOnError false
+    }
+
+    defaultConfig {
+        testInstrumentationRunner "com.google.android.apps.common.testing.testrunner.GoogleInstrumentationTestRunner"
+    }
+
+    sourceSets {
+        // Setting espresso-sample as the main root of this project to avoid source code duplication.
+        // Temporary workaround until Android Gradle plugin supports settings custom target package
+        // for Android Tests.
+        main.setRoot("../espresso-sample/src/main")
+    }
+}
+
+dependencies {
+    compile files('../libs/guava-14.0.1.jar')
+    compile 'com.android.support:support-v4:19.1.+'
+    compile 'com.android.support:appcompat-v7:19.1.+'
+
+    androidTestCompile project(':espresso-contrib')
+}
diff --git a/espresso/espresso-contrib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/contrib/DrawerActionsIntegrationTest.java b/espresso/espresso-contrib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/contrib/DrawerActionsIntegrationTest.java
new file mode 100644
index 0000000..0e1ee59
--- /dev/null
+++ b/espresso/espresso-contrib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/contrib/DrawerActionsIntegrationTest.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.contrib;
+
+import static com.google.android.apps.common.testing.ui.espresso.Espresso.onData;
+import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView;
+import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click;
+import static com.google.android.apps.common.testing.ui.espresso.assertion.ViewAssertions.matches;
+import static com.google.android.apps.common.testing.ui.espresso.contrib.DrawerActions.closeDrawer;
+import static com.google.android.apps.common.testing.ui.espresso.contrib.DrawerActions.openDrawer;
+import static com.google.android.apps.common.testing.ui.espresso.contrib.DrawerMatchers.isClosed;
+import static com.google.android.apps.common.testing.ui.espresso.contrib.DrawerMatchers.isOpen;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withText;
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.is;
+
+import com.google.android.apps.common.testing.ui.testapp.DrawerActivity;
+import com.google.android.apps.common.testing.ui.testapp.R;
+
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+
+/**
+ * Integration tests for {@link DrawerActions}.
+ */
+@LargeTest
+public class DrawerActionsIntegrationTest extends ActivityInstrumentationTestCase2<DrawerActivity> {
+
+  public DrawerActionsIntegrationTest() {
+    super(DrawerActivity.class);
+  }
+
+  @Override
+  protected void setUp() throws Exception {
+    super.setUp();
+    getActivity();
+  }
+
+  public void testOpenAndCloseDrawer() {
+    // Drawer should not be open to start.
+    onView(withId(R.id.drawer_layout)).check(matches(isClosed()));
+
+    openDrawer(R.id.drawer_layout);
+
+    // The drawer should now be open.
+    onView(withId(R.id.drawer_layout)).check(matches(isOpen()));
+
+    closeDrawer(R.id.drawer_layout);
+
+    // Drawer should be closed again.
+    onView(withId(R.id.drawer_layout)).check(matches(isClosed()));
+  }
+
+  public void testOpenAndCloseDrawer_idempotent() {
+    // Drawer should not be open to start.
+    onView(withId(R.id.drawer_layout)).check(matches(isClosed()));
+
+    // Open drawer repeatedly.
+    openDrawer(R.id.drawer_layout);
+    openDrawer(R.id.drawer_layout);
+    openDrawer(R.id.drawer_layout);
+
+    // The drawer should be open.
+    onView(withId(R.id.drawer_layout)).check(matches(isOpen()));
+
+    // Close drawer repeatedly.
+    closeDrawer(R.id.drawer_layout);
+    closeDrawer(R.id.drawer_layout);
+    closeDrawer(R.id.drawer_layout);
+
+    // Drawer should be closed.
+    onView(withId(R.id.drawer_layout)).check(matches(isClosed()));
+  }
+
+  @SuppressWarnings("unchecked")
+  public void testOpenDrawer_clickItem() {
+    openDrawer(R.id.drawer_layout);
+
+    // Click an item in the drawer.
+    int rowIndex = 2;
+    String rowContents = DrawerActivity.DRAWER_CONTENTS[rowIndex];
+    onData(allOf(is(instanceOf(String.class)), is(rowContents))).perform(click());
+
+    // clicking the item should close the drawer.
+    onView(withId(R.id.drawer_layout)).check(matches(isClosed()));
+
+    // The text view will now display "You picked: Pickle"
+    onView(withId(R.id.drawer_text_view)).check(matches(withText("You picked: " + rowContents)));
+  }
+}
diff --git a/espresso/espresso-contrib/build.gradle b/espresso/espresso-contrib/build.gradle
new file mode 100644
index 0000000..5a4d4eb
--- /dev/null
+++ b/espresso/espresso-contrib/build.gradle
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * 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.
+ */
+
+apply plugin: 'android-library'
+
+sourceCompatibility = JavaVersion.VERSION_1_5
+targetCompatibility = JavaVersion.VERSION_1_5
+
+repositories {
+    maven { url '../../../../prebuilts/tools/common/m2/repository' }
+    maven { url '../../../../prebuilts/tools/common/m2/internal' }
+}
+
+android {
+    compileSdkVersion 19
+    buildToolsVersion "19.0.3"
+
+    packagingOptions {
+        exclude 'LICENSE.txt'
+    }
+
+    lintOptions {
+        abortOnError false
+    }
+}
+
+dependencies {
+    compile project(':espresso-lib')
+    compile 'com.android.support:support-v4:19.1.+'
+}
diff --git a/espresso/espresso-contrib/src/main/AndroidManifest.xml b/espresso/espresso-contrib/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..1dd537b
--- /dev/null
+++ b/espresso/espresso-contrib/src/main/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2014 The Android Open Source Project
+  ~
+  ~ 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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.google.android.apps.common.testing.ui.espresso.contrib" >
+
+    <uses-sdk
+        android:minSdkVersion="7"/>
+
+    <application />
+
+</manifest>
\ No newline at end of file
diff --git a/espresso/espresso-contrib/src/main/java/com/google/android/apps/common/testing/ui/espresso/contrib/DrawerActions.java b/espresso/espresso-contrib/src/main/java/com/google/android/apps/common/testing/ui/espresso/contrib/DrawerActions.java
new file mode 100644
index 0000000..733c94f
--- /dev/null
+++ b/espresso/espresso-contrib/src/main/java/com/google/android/apps/common/testing/ui/espresso/contrib/DrawerActions.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.contrib;
+import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView;
+import static com.google.android.apps.common.testing.ui.espresso.contrib.DrawerMatchers.isClosed;
+import static com.google.android.apps.common.testing.ui.espresso.contrib.DrawerMatchers.isOpen;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isAssignableFrom;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId;
+
+import com.google.android.apps.common.testing.ui.espresso.Espresso;
+import com.google.android.apps.common.testing.ui.espresso.IdlingResource;
+import com.google.android.apps.common.testing.ui.espresso.PerformException;
+import com.google.android.apps.common.testing.ui.espresso.UiController;
+import com.google.android.apps.common.testing.ui.espresso.ViewAction;
+
+import android.support.v4.view.GravityCompat;
+import android.support.v4.widget.DrawerLayout;
+import android.support.v4.widget.DrawerLayout.DrawerListener;
+import android.view.View;
+
+import org.hamcrest.Matcher;
+
+import java.lang.reflect.Field;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.annotation.Nullable;
+
+/**
+ * Espresso actions for using a {@link DrawerLayout}.
+ *
+ * @see <a href="http://developer.android.com/design/patterns/navigation-drawer.html">Navigation
+ *      drawer design guide</a>
+ */
+public final class DrawerActions {
+
+  private DrawerActions() {
+    // forbid instantiation
+  }
+
+  private static Field listenerField;
+
+  /**
+   * Opens the {@link DrawerLayout} with the given id. This method blocks until the drawer is fully
+   * open. No operation if the drawer is already open.
+   */
+  public static void openDrawer(int drawerLayoutId) {
+    //if the drawer is already open, return.
+    if (checkDrawer(drawerLayoutId, isOpen())) {
+      return;
+    }
+    onView(withId(drawerLayoutId)).perform(registerListener());
+    onView(withId(drawerLayoutId)).perform(actionOpenDrawer());
+  }
+
+  /**
+   * Closes the {@link DrawerLayout} with the given id. This method blocks until the drawer is fully
+   * closed. No operation if the drawer is already closed.
+   */
+  public static void closeDrawer(int drawerLayoutId) {
+    //if the drawer is already closed, return.
+    if (checkDrawer(drawerLayoutId, isClosed())) {
+      return;
+    }
+    onView(withId(drawerLayoutId)).perform(registerListener());
+    onView(withId(drawerLayoutId)).perform(actionCloseDrawer());
+  }
+
+  /**
+   * Returns true if the given matcher matches the drawer.
+   */
+  private static boolean checkDrawer(int drawerLayoutId, final Matcher<View> matcher) {
+    final AtomicBoolean matches = new AtomicBoolean(false);
+    onView(withId(drawerLayoutId)).perform(new ViewAction() {
+
+      @Override
+      public Matcher<View> getConstraints() {
+        return isAssignableFrom(DrawerLayout.class);
+      }
+
+      @Override
+      public String getDescription() {
+        return "check drawer";
+      }
+
+      @Override
+      public void perform(UiController uiController, View view) {
+        matches.set(matcher.matches(view));
+      }
+    });
+    return matches.get();
+  }
+
+  private static ViewAction actionOpenDrawer() {
+    return new ViewAction() {
+      @Override
+      public Matcher<View> getConstraints() {
+        return isAssignableFrom(DrawerLayout.class);
+      }
+
+      @Override
+      public String getDescription() {
+        return "open drawer";
+      }
+
+      @Override
+      public void perform(UiController uiController, View view) {
+        ((DrawerLayout) view).openDrawer(GravityCompat.START);
+      }
+    };
+  }
+
+  private static ViewAction actionCloseDrawer() {
+    return new ViewAction() {
+      @Override
+      public Matcher<View> getConstraints() {
+        return isAssignableFrom(DrawerLayout.class);
+      }
+
+      @Override
+      public String getDescription() {
+        return "close drawer";
+      }
+
+      @Override
+      public void perform(UiController uiController, View view) {
+        ((DrawerLayout) view).closeDrawer(GravityCompat.START);
+      }
+    };
+  }
+
+  /**
+   * Returns a {@link ViewAction} that adds an {@link IdlingDrawerListener} as a drawer listener to
+   * the {@link DrawerLayout}. The idling drawer listener wraps any listener that already exists.
+   */
+  private static ViewAction registerListener() {
+    return new ViewAction() {
+      @Override
+      public Matcher<View> getConstraints() {
+        return isAssignableFrom(DrawerLayout.class);
+      }
+
+      @Override
+      public String getDescription() {
+        return "register idling drawer listener";
+      }
+
+      @Override
+      public void perform(UiController uiController, View view) {
+        DrawerLayout drawer = (DrawerLayout) view;
+        DrawerListener existingListener = getDrawerListener(drawer);
+        if (existingListener instanceof IdlingDrawerListener) {
+          // listener is already registered. No need to assign.
+          return;
+        }
+        drawer.setDrawerListener(IdlingDrawerListener.getInstance(existingListener));
+      }
+    };
+  }
+
+  /**
+   * Pries the current {@link DrawerListener} loose from the cold dead hands of the given
+   * {@link DrawerLayout}. Uses reflection.
+   */
+  @Nullable
+  private static DrawerListener getDrawerListener(DrawerLayout drawer) {
+    try {
+      if (listenerField == null) {
+        // lazy initialization of reflected field.
+        listenerField = DrawerLayout.class.getDeclaredField("mListener");
+        listenerField.setAccessible(true);
+      }
+      return (DrawerListener) listenerField.get(drawer);
+    } catch (IllegalArgumentException ex) {
+      // Pity we can't use Java 7 multi-catch for all of these.
+      throw new PerformException.Builder().withCause(ex).build();
+    } catch (IllegalAccessException ex) {
+      throw new PerformException.Builder().withCause(ex).build();
+    } catch (NoSuchFieldException ex) {
+      throw new PerformException.Builder().withCause(ex).build();
+    } catch (SecurityException ex) {
+      throw new PerformException.Builder().withCause(ex).build();
+    }
+  }
+
+  /**
+   * Drawer listener that wraps an existing {@link DrawerListener}, and functions as an
+   * {@link IdlingResource} for Espresso.
+   */
+  private static class IdlingDrawerListener implements DrawerListener, IdlingResource {
+
+    private static IdlingDrawerListener instance;
+    private static IdlingDrawerListener getInstance(DrawerListener parentListener) {
+      if (instance == null) {
+        instance = new IdlingDrawerListener();
+        Espresso.registerIdlingResources(instance);
+      }
+      instance.setParentListener(parentListener);
+      return instance;
+    }
+
+    @Nullable private DrawerListener parentListener;
+    private ResourceCallback callback;
+    // Idle state is only accessible from main thread.
+    private boolean idle = true;
+
+    public void setParentListener(@Nullable DrawerListener parentListener) {
+      this.parentListener = parentListener;
+    }
+
+    @Override
+    public void onDrawerClosed(View drawer) {
+      if (parentListener != null) {
+        parentListener.onDrawerClosed(drawer);
+      }
+    }
+
+    @Override
+    public void onDrawerOpened(View drawer) {
+      if (parentListener != null) {
+        parentListener.onDrawerOpened(drawer);
+      }
+    }
+
+    @Override
+    public void onDrawerSlide(View drawer, float slideOffset) {
+      if (parentListener != null) {
+        parentListener.onDrawerSlide(drawer, slideOffset);
+      }
+    }
+
+    @Override
+    public void onDrawerStateChanged(int newState) {
+      if (newState == DrawerLayout.STATE_IDLE) {
+        idle = true;
+        if (callback != null) {
+          callback.onTransitionToIdle();
+        }
+      } else {
+        idle = false;
+      }
+      if (parentListener != null) {
+        parentListener.onDrawerStateChanged(newState);
+      }
+    }
+
+    @Override
+    public String getName() {
+      return "IdlingDrawerListener";
+    }
+
+    @Override
+    public boolean isIdleNow() {
+      return idle;
+    }
+
+    @Override
+    public void registerIdleTransitionCallback(ResourceCallback callback) {
+      this.callback = callback;
+    }
+  }
+}
diff --git a/espresso/espresso-contrib/src/main/java/com/google/android/apps/common/testing/ui/espresso/contrib/DrawerMatchers.java b/espresso/espresso-contrib/src/main/java/com/google/android/apps/common/testing/ui/espresso/contrib/DrawerMatchers.java
new file mode 100644
index 0000000..ca66af8
--- /dev/null
+++ b/espresso/espresso-contrib/src/main/java/com/google/android/apps/common/testing/ui/espresso/contrib/DrawerMatchers.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.contrib;
+
+import com.google.android.apps.common.testing.ui.espresso.matcher.BoundedMatcher;
+
+import android.support.v4.view.GravityCompat;
+import android.support.v4.widget.DrawerLayout;
+import android.view.View;
+
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+
+/**
+ * Hamcrest matchers for a {@link DrawerLayout}.
+ */
+public final class DrawerMatchers {
+
+  private DrawerMatchers() {
+    // forbid instantiation
+  }
+
+  /**
+   * Returns a matcher that verifies that the drawer is open. Matches only when the drawer is fully
+   * open. Use {@link #isClosed()} instead of {@code not(isOpen())} when you wish to check that the
+   * drawer is fully closed.
+   */
+  public static Matcher<View> isOpen() {
+    return new BoundedMatcher<View, DrawerLayout>(DrawerLayout.class) {
+      @Override
+      public void describeTo(Description description) {
+        description.appendText("is drawer open");
+      }
+
+      @Override
+      public boolean matchesSafely(DrawerLayout drawer) {
+        return drawer.isDrawerOpen(GravityCompat.START);
+      }
+    };
+  }
+
+  /**
+   * Returns a matcher that verifies that the drawer is closed. Matches only when the drawer is
+   * fully closed. Use {@link #isOpen()} instead of {@code not(isClosed()))} when you wish to check
+   * that the drawer is fully open.
+   */
+  public static Matcher<View> isClosed() {
+    return new BoundedMatcher<View, DrawerLayout>(DrawerLayout.class) {
+      @Override
+      public void describeTo(Description description) {
+        description.appendText("is drawer closed");
+      }
+
+      @Override
+      public boolean matchesSafely(DrawerLayout drawer) {
+        return !drawer.isDrawerVisible(GravityCompat.START);
+      }
+    };
+  }
+}
diff --git a/espresso/espresso-lib-tests/build.gradle b/espresso/espresso-lib-tests/build.gradle
new file mode 100644
index 0000000..ce3faa7
--- /dev/null
+++ b/espresso/espresso-lib-tests/build.gradle
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * 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.
+ */
+
+apply plugin: 'android'
+
+repositories {
+    maven { url '../../../../prebuilts/tools/common/m2/repository' }
+    maven { url '../../../../prebuilts/tools/common/m2/internal' }
+}
+
+android {
+    compileSdkVersion 19
+    buildToolsVersion "19.0.3"
+
+    packagingOptions {
+        exclude 'LICENSE.txt'
+    }
+
+    lintOptions {
+        abortOnError false
+    }
+
+    defaultConfig {
+        testInstrumentationRunner "com.google.android.apps.common.testing.testrunner.GoogleInstrumentationTestRunner"
+    }
+
+    sourceSets {
+        // Setting espresso-sample as the main root of this project to avoid source code duplication.
+        // Temporary workaround until Android Gradle plugin supports settings custom target package
+        // for Android Tests.
+        main.setRoot("../espresso-sample/src/main")
+    }
+}
+
+dependencies {
+    compile files('../libs/guava-14.0.1.jar')
+    compile 'com.android.support:support-v4:19.1.+'
+    compile 'com.android.support:appcompat-v7:19.1.+'
+
+    // run test against an un-jarjared variant of the lib
+    androidTestCompile project(path: ':espresso-lib', configuration: 'debug')
+
+    androidTestCompile 'org.mockito:mockito-core:1.9.5'
+}
diff --git a/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/AmbiguousViewMatcherExceptionTest.java b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/AmbiguousViewMatcherExceptionTest.java
new file mode 100644
index 0000000..650c426
--- /dev/null
+++ b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/AmbiguousViewMatcherExceptionTest.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.containsString;
+
+import android.test.AndroidTestCase;
+import android.view.View;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import org.hamcrest.Matcher;
+import org.hamcrest.Matchers;
+import org.hamcrest.StringDescription;
+
+/** Unit tests for {@link AmbiguousViewMatcherException}. */
+public class AmbiguousViewMatcherExceptionTest extends AndroidTestCase {
+  private Matcher<View> alwaysTrueMatcher;
+
+  private RelativeLayout testView;
+  private View child1;
+  private View child2;
+  private View child3;
+  private View child4;
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    alwaysTrueMatcher = Matchers.<View>notNullValue();
+    testView = new RelativeLayout(getContext());
+    child1 = new TextView(getContext());
+    child1.setId(1);
+    child2 = new TextView(getContext());
+    child2.setId(2);
+    child3 = new TextView(getContext());
+    child3.setId(3);
+    child4 = new TextView(getContext());
+    child4.setId(4);
+    testView.addView(child1);
+    testView.addView(child2);
+    testView.addView(child3);
+    testView.addView(child4);
+  }
+
+  public void testExceptionContainsMatcherDescription() {
+    StringBuilder matcherDescription = new StringBuilder();
+    alwaysTrueMatcher.describeTo(new StringDescription(matcherDescription));
+    assertThat(createException().getMessage(), containsString(matcherDescription.toString()));
+  }
+
+  @SuppressWarnings("unchecked")
+  public void testExceptionContainsView() {
+    String exceptionMessage = createException().getMessage();
+
+    assertThat("missing elements", exceptionMessage,
+       allOf(
+         containsString("{id=1,"), // child1
+         containsString("{id=2,"), // child2
+         containsString("{id=3,"), // child3
+         containsString("{id=4,"), // child4
+         containsString("{id=-1,"))); // root
+  }
+
+  private AmbiguousViewMatcherException createException() {
+
+    return new AmbiguousViewMatcherException.Builder()
+        .withViewMatcher(alwaysTrueMatcher)
+        .withRootView(testView)
+        .withView1(testView)
+        .withView2(child1)
+        .withOtherAmbiguousViews(child2, child3, child4)
+        .build();
+  }
+}
diff --git a/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/AppNotIdleExceptionTest.java b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/AppNotIdleExceptionTest.java
new file mode 100644
index 0000000..48fe347
--- /dev/null
+++ b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/AppNotIdleExceptionTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso;
+
+import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView;
+import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId;
+
+import com.google.android.apps.common.testing.ui.testapp.R;
+import com.google.android.apps.common.testing.ui.testapp.SyncActivity;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Test case for {@link AppNotIdleException}.
+ */
+@LargeTest
+public class AppNotIdleExceptionTest extends ActivityInstrumentationTestCase2<SyncActivity> {
+
+  @SuppressWarnings("deprecation")
+  public AppNotIdleExceptionTest() {
+    // This constructor was deprecated - but we want to support lower API levels.
+    super("com.google.android.apps.common.testing.ui.testapp", SyncActivity.class);
+  }
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    getActivity();
+  }
+
+  public void testAppIdleException() throws Exception {
+    final AtomicBoolean continueBeingBusy = new AtomicBoolean(true);
+    try {
+      final Handler handler = new Handler(Looper.getMainLooper());
+      Runnable runnable = new Runnable() {
+        @Override
+        public void run() {
+          if (!continueBeingBusy.get()) {
+            return;
+          } else {
+            handler.post(this);
+          }
+        }
+      };
+      FutureTask<Void> task = new FutureTask<Void>(runnable, null);
+      handler.post(task);
+      task.get(); // Will Make sure that the first post is sent before we do a lookup.
+      // Request the "hello world!" text by clicking on the request button.
+      onView(withId(R.id.request_button)).perform(click());
+      fail("Espresso failed to throw AppNotIdleException");
+    } catch (AppNotIdleException e) {
+      // Do Nothing. Test pass.
+      continueBeingBusy.getAndSet(false);
+    }
+  }
+}
diff --git a/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/EspressoEdgeCaseTest.java b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/EspressoEdgeCaseTest.java
new file mode 100644
index 0000000..8439a96
--- /dev/null
+++ b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/EspressoEdgeCaseTest.java
@@ -0,0 +1,308 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso;
+
+import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView;
+import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click;
+import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.typeText;
+import static com.google.android.apps.common.testing.ui.espresso.assertion.ViewAssertions.matches;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isAssignableFrom;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withText;
+
+import com.google.android.apps.common.testing.ui.testapp.R;
+import com.google.android.apps.common.testing.ui.testapp.SendActivity;
+
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.view.View;
+
+import org.hamcrest.Matcher;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Collection of some nasty edge cases.
+ */
+@LargeTest
+public class EspressoEdgeCaseTest extends ActivityInstrumentationTestCase2<SendActivity> {
+  @SuppressWarnings("deprecation")
+  public EspressoEdgeCaseTest() {
+    // Supporting froyo.
+    super("com.google.android.apps.common.testing.ui.testapp", SendActivity.class);
+  }
+
+  private static final Callable<Void> NO_OP = new Callable<Void>() {
+    @Override
+    public Void call() {
+      return null;
+    }
+  };
+
+  private Handler mainHandler;
+  private OneShotResource oneShotResource;
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    getActivity();
+    mainHandler = new Handler(Looper.getMainLooper());
+    oneShotResource = new OneShotResource();
+  }
+
+  @Override
+  public void tearDown() throws Exception {
+    IdlingPolicies.setMasterPolicyTimeout(60, TimeUnit.SECONDS);
+    IdlingPolicies.setIdlingResourceTimeout(26, TimeUnit.SECONDS);
+    oneShotResource.setIdle(true);
+    super.tearDown();
+  }
+
+  @SuppressWarnings("unchecked")
+  public void testRecoveryFromExceptionOnMainThreadLoopMainThreadUntilIdle() throws Exception {
+    final RuntimeException poison = new RuntimeException("oops");
+    try {
+      onView(withId(R.id.enter_data_edit_text))
+          .perform(
+              new TestAction() {
+
+                @Override
+                public void perform(UiController controller, View view) {
+                  mainHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                      throw poison;
+                    }});
+                  controller.loopMainThreadUntilIdle();
+                }
+              });
+      fail("should throw");
+    } catch (RuntimeException re) {
+      if (re == poison) {
+        // expected
+      } else {
+        // something else.
+        throw re;
+      }
+    }
+    // life should continue normally.
+    onView(withId(R.id.enter_data_edit_text))
+        .perform(typeText("Hello World111"));
+    onView(withId(R.id.enter_data_edit_text))
+        .check(matches(withText("Hello World111")));
+  }
+
+  @SuppressWarnings("unchecked")
+  public void testRecoveryFromExceptionOnMainThreadLoopMainThreadForAtLeast() throws Exception {
+    final RuntimeException poison = new RuntimeException("oops");
+    final FutureTask<Void> syncTask = new FutureTask<Void>(NO_OP);
+    try {
+      onView(withId(R.id.enter_data_edit_text))
+          .perform(
+              new TestAction() {
+                @Override
+                public void perform(UiController controller, View view) {
+                  mainHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                      throw poison;
+                    }});
+                  // block test execution until loopMainThreadForAtLeast call
+                  // would be satisified
+                  mainHandler.postDelayed(syncTask, 2500);
+                  controller.loopMainThreadForAtLeast(2000);
+                }
+              });
+      fail("should throw");
+    } catch (RuntimeException re) {
+      if (re == poison) {
+        // expected
+      } else {
+        // something else.
+        throw re;
+      }
+    }
+    syncTask.get();
+
+    // life should continue normally.
+    onView(withId(R.id.enter_data_edit_text))
+        .perform(typeText("baz bar"));
+    onView(withId(R.id.enter_data_edit_text))
+        .check(matches(withText("baz bar")));
+  }
+
+  @SuppressWarnings("unchecked")
+  public void testRecoveryFromTimeOutExceptionMaster() throws Exception {
+    IdlingPolicies.setMasterPolicyTimeout(2, TimeUnit.SECONDS);
+    final FutureTask<Void> syncTask = new FutureTask<Void>(NO_OP);
+    try {
+      onView(withId(R.id.enter_data_edit_text))
+          .perform(
+              new TestAction() {
+                @Override
+                public void perform(UiController controller, View view) {
+                  mainHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                      SystemClock.sleep(TimeUnit.SECONDS.toMillis(8));
+                    }
+                  });
+                  // block test execution until loopMainThreadForAtLeast call
+                  // would be satisified
+                  mainHandler.postDelayed(syncTask, 2500);
+                  controller.loopMainThreadForAtLeast(1000);
+                }
+              });
+      fail("should throw");
+    } catch (RuntimeException re) {
+      if (re instanceof EspressoException) {
+        // expected
+      } else {
+        // something else.
+        throw re;
+      }
+    }
+    syncTask.get();
+
+    // life should continue normally.
+    onView(withId(R.id.enter_data_edit_text))
+        .perform(typeText("one two three"));
+    onView(withId(R.id.enter_data_edit_text))
+        .check(matches(withText("one two three")));
+  }
+
+  @SuppressWarnings("unchecked")
+  public void testRecoveryFromTimeOutExceptionDynamic() {
+    IdlingPolicies.setIdlingResourceTimeout(2, TimeUnit.SECONDS);
+
+    Espresso.registerIdlingResources(oneShotResource);
+    oneShotResource.setIdle(false);
+
+    try {
+      onView(withId(R.id.enter_data_edit_text))
+          .perform(click());
+      fail("should throw");
+    } catch (RuntimeException re) {
+      if (re instanceof EspressoException) {
+        // expected
+      } else {
+        // something else.
+        throw re;
+      }
+    }
+    oneShotResource.setIdle(true);
+
+    // life should continue normally.
+    onView(withId(R.id.enter_data_edit_text))
+        .perform(typeText("Doh"));
+    onView(withId(R.id.enter_data_edit_text))
+        .check(matches(withText("Doh")));
+  }
+
+  public void testRecoveryFromAsyncTaskTimeout() throws Exception {
+    IdlingPolicies.setMasterPolicyTimeout(2, TimeUnit.SECONDS);
+    try {
+      onView(withId(R.id.enter_data_edit_text))
+          .perform(new TestAction() {
+            @Override
+            public void perform(UiController controller, View view) {
+              new AsyncTask<Void, Void, Void>() {
+                @Override
+                public Void doInBackground(Void... params) {
+                  SystemClock.sleep(TimeUnit.SECONDS.toMillis(8));
+                  return null;
+                }
+              }.execute();
+              // block test execution until loopMainThreadForAtLeast call
+              // would be satisified
+              controller.loopMainThreadForAtLeast(1000);
+            }
+          });
+      fail("should throw");
+    } catch (RuntimeException re) {
+      if (re instanceof EspressoException) {
+        // expected
+      } else {
+        // something else.
+        throw re;
+      }
+    }
+    IdlingPolicies.setMasterPolicyTimeout(60, TimeUnit.SECONDS);
+    // life should continue normally.
+    onView(withId(R.id.enter_data_edit_text))
+        .perform(typeText("Har Har"));
+    onView(withId(R.id.enter_data_edit_text))
+        .check(matches(withText("Har Har")));
+  }
+
+
+
+
+  private abstract static class TestAction implements ViewAction {
+    @Override
+    public String getDescription() {
+      return "A random test action.";
+    }
+
+    @Override
+    public Matcher<View> getConstraints() {
+      return isAssignableFrom(View.class);
+    }
+  }
+
+
+  private static class OneShotResource implements IdlingResource {
+    private static AtomicInteger counter = new AtomicInteger(0);
+
+    private final int instance;
+    private volatile IdlingResource.ResourceCallback callback;
+    private volatile boolean isIdle = true;
+
+    private OneShotResource() {
+      instance = counter.incrementAndGet();
+    }
+
+    @Override
+    public String getName() {
+      return "TestOneShotResource_" + counter;
+    }
+
+    public void setIdle(boolean idle) {
+      isIdle = idle;
+      if (isIdle && callback != null) {
+        callback.onTransitionToIdle();
+      }
+    }
+
+    @Override
+    public boolean isIdleNow() {
+      return isIdle;
+    }
+
+    @Override
+    public void registerIdleTransitionCallback(IdlingResource.ResourceCallback callback) {
+      this.callback = callback;
+    }
+  }
+}
diff --git a/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/EspressoTest.java b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/EspressoTest.java
new file mode 100644
index 0000000..ff4ff39
--- /dev/null
+++ b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/EspressoTest.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso;
+
+import static com.google.android.apps.common.testing.ui.espresso.Espresso.onData;
+import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView;
+import static com.google.android.apps.common.testing.ui.espresso.Espresso.openActionBarOverflowOrOptionsMenu;
+import static com.google.android.apps.common.testing.ui.espresso.Espresso.openContextualActionModeOverflowMenu;
+import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click;
+import static com.google.android.apps.common.testing.ui.espresso.assertion.ViewAssertions.matches;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withText;
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.anything;
+import static org.hamcrest.Matchers.hasValue;
+import static org.hamcrest.Matchers.instanceOf;
+
+import com.google.android.apps.common.testing.ui.espresso.action.ViewActions;
+import com.google.android.apps.common.testing.ui.testapp.ActionBarTestActivity;
+import com.google.android.apps.common.testing.ui.testapp.MainActivity;
+import com.google.android.apps.common.testing.ui.testapp.R;
+import com.google.android.apps.common.testing.ui.testapp.SendActivity;
+
+import android.content.Context;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+
+import org.hamcrest.Matcher;
+
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Tests Espresso top level (i.e. ones not specific to a view) actions like pressBack and
+ * closeSoftKeyboard.
+ */
+@LargeTest
+public class EspressoTest extends ActivityInstrumentationTestCase2<MainActivity> {
+  @SuppressWarnings("deprecation")
+  public EspressoTest() {
+    // Supporting froyo.
+    super("com.google.android.apps.common.testing.ui.testapp", MainActivity.class);
+  }
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    getActivity();
+  }
+
+  @SuppressWarnings("unchecked")
+  public void testOpenOverflowInActionMode() {
+    onData(allOf(instanceOf(Map.class), hasValue(ActionBarTestActivity.class.getSimpleName())))
+        .perform(click());
+    openContextualActionModeOverflowMenu();
+    onView(withText("Key"))
+        .perform(click());
+    onView(withId(R.id.text_action_bar_result))
+        .check(matches(withText("Key")));
+  }
+
+  @SuppressWarnings("unchecked")
+  public void testOpenOverflowFromActionBar() {
+    onData(allOf(instanceOf(Map.class), hasValue(ActionBarTestActivity.class.getSimpleName())))
+        .perform(click());
+    onView(withId(R.id.hide_contextual_action_bar))
+        .perform(click());
+    openActionBarOverflowOrOptionsMenu(getInstrumentation().getTargetContext());
+    onView(withText("World"))
+        .perform(click());
+    onView(withId(R.id.text_action_bar_result))
+        .check(matches(withText("World")));
+  }
+
+  @SuppressWarnings("unchecked")
+  public void testCloseSoftKeyboard() {
+    onData(allOf(instanceOf(Map.class), hasValue(SendActivity.class.getSimpleName())))
+        .perform(click());
+
+    onView(withId(R.id.enter_data_edit_text)).perform(new ViewAction() {
+      @Override
+      public Matcher<View> getConstraints() {
+        return anything();
+      }
+
+      @Override
+      public void perform(UiController uiController, View view) {
+        InputMethodManager imm = (InputMethodManager) getInstrumentation().getTargetContext()
+          .getSystemService(Context.INPUT_METHOD_SERVICE);
+        imm.showSoftInput(view, 0);
+        uiController.loopMainThreadUntilIdle();
+      }
+
+      @Override
+      public String getDescription() {
+        return "show soft input";
+      }
+    });
+
+    onView(withId(R.id.enter_data_edit_text)).perform(ViewActions.closeSoftKeyboard());
+  }
+
+  public void testSetFailureHandler() {
+    final AtomicBoolean handled = new AtomicBoolean(false);
+    Espresso.setFailureHandler(new FailureHandler() {
+      @Override
+      public void handle(Throwable error, Matcher<View> viewMatcher) {
+        handled.set(true);
+      }
+    });
+    onView(withText("does not exist")).perform(click());
+    assertTrue(handled.get());
+  }
+
+  public void testRegisterResourceWithNullName() {
+    try {
+      Espresso.registerIdlingResources(new IdlingResource() {
+        @Override
+        public boolean isIdleNow() {
+          return true;
+        }
+
+        @Override
+        public String getName() {
+          return null;
+        }
+
+       @Override
+       public void registerIdleTransitionCallback(ResourceCallback callback) {
+         // ignore
+       }
+      });
+      fail("Should have thrown NPE");
+    } catch (NullPointerException expected) {}
+  }
+}
diff --git a/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/NoMatchingViewExceptionTest.java b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/NoMatchingViewExceptionTest.java
new file mode 100644
index 0000000..16571e3
--- /dev/null
+++ b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/NoMatchingViewExceptionTest.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+
+import android.test.AndroidTestCase;
+import android.view.View;
+
+import org.hamcrest.Matcher;
+import org.hamcrest.Matchers;
+import org.hamcrest.StringDescription;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link NoMatchingViewException}. */
+public class NoMatchingViewExceptionTest extends AndroidTestCase {
+  private Matcher<View> alwaysFailingMatcher;
+
+  @Mock
+  private View testView;
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    MockitoAnnotations.initMocks(this);
+    alwaysFailingMatcher = Matchers.<View>nullValue();
+  }
+
+  public void testExceptionContainsMatcherDescription() {
+    StringBuilder matcherDescription = new StringBuilder();
+    alwaysFailingMatcher.describeTo(new StringDescription(matcherDescription));
+    assertThat(createException().getMessage(), containsString(matcherDescription.toString()));
+  }
+
+  public void testExceptionContainsView() {
+    String exceptionMessage = createException().getMessage();
+
+    assertThat("missing root element" + exceptionMessage, exceptionMessage,
+        containsString("{id=0,"));
+  }
+
+  private NoMatchingViewException createException() {
+    return new NoMatchingViewException.Builder()
+        .withViewMatcher(alwaysFailingMatcher)
+        .withRootView(testView)
+        .build();
+  }
+}
diff --git a/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/UnitTests.java b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/UnitTests.java
new file mode 100644
index 0000000..b3c3f98
--- /dev/null
+++ b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/UnitTests.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso;
+
+import android.test.suitebuilder.TestSuiteBuilder;
+
+import junit.framework.Test;
+import junit.framework.TestSuite;
+
+/**
+ * TestSuite containing "unit tests" for the UI Framework.
+ *
+ */
+public class UnitTests extends TestSuite {
+  public static Test suite() {
+    return new TestSuiteBuilder(UnitTests.class)
+      .includeAllPackagesUnderHere()
+      .build();
+  }
+}
diff --git a/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/ViewInteractionTest.java b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/ViewInteractionTest.java
new file mode 100755
index 0000000..295572c
--- /dev/null
+++ b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/ViewInteractionTest.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso;
+
+import static com.google.common.base.Throwables.propagate;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.nullValue;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import com.google.android.apps.common.testing.testrunner.ActivityLifecycleMonitor;
+import com.google.android.apps.common.testing.testrunner.ActivityLifecycleMonitorRegistry;
+import com.google.android.apps.common.testing.ui.espresso.matcher.RootMatchers;
+import com.google.common.util.concurrent.MoreExecutors;
+
+import android.test.AndroidTestCase;
+import android.view.View;
+
+import org.hamcrest.Matcher;
+import org.hamcrest.Matchers;
+import org.mockito.Mock;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicReference;
+
+/** Unit tests for {@link ViewInteraction}. */
+public class ViewInteractionTest extends AndroidTestCase {
+  @Mock
+  private ViewFinder mockViewFinder;
+  @Mock
+  private ViewAssertion mockAssertion;
+  @Mock
+  private ViewAction mockAction;
+  @Mock
+  private UiController mockUiController;
+
+  
+  private FailureHandler failureHandler;
+  private Executor testExecutor = MoreExecutors.sameThreadExecutor();
+
+  private ActivityLifecycleMonitor realLifecycleMonitor;
+  private ViewInteraction testInteraction;
+  private View rootView;
+  private View targetView;
+  private Matcher<View> viewMatcher;
+  private Matcher<View> actionConstraint;
+  private AtomicReference<Matcher<Root>> rootMatcherRef;
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    initMocks(this);
+    realLifecycleMonitor = ActivityLifecycleMonitorRegistry.getInstance();
+    rootView = new View(getContext());
+    targetView = new View(getContext());
+    viewMatcher = is(targetView);
+    actionConstraint = Matchers.<View>notNullValue();
+    rootMatcherRef = new AtomicReference<Matcher<Root>>(RootMatchers.DEFAULT);
+    when(mockAction.getDescription()).thenReturn("A Mock!");
+    failureHandler = new FailureHandler() {
+      @Override
+      public void handle(Throwable error, Matcher<View> viewMatcher) {
+        propagate(error);
+      }
+    };
+  }
+
+  @Override
+  public void tearDown() throws Exception {
+    ActivityLifecycleMonitorRegistry.registerInstance(realLifecycleMonitor);
+    super.tearDown();
+  }
+
+  public void testPerformViewViolatesConstraints() {
+    actionConstraint = not(viewMatcher);
+    when(mockViewFinder.getView()).thenReturn(targetView);
+    initInteraction();
+    try {
+      testInteraction.perform(mockAction);
+      fail("should propagate constraint violation!");
+    } catch (RuntimeException re) {
+      if (!PerformException.class.isAssignableFrom(re.getClass())) {
+        throw re;
+      }
+    }
+  }
+
+  public void testPerformPropagatesException() {
+    RuntimeException exceptionToRaise = new RuntimeException();
+    when(mockViewFinder.getView()).thenReturn(targetView);
+    doThrow(exceptionToRaise)
+        .when(mockAction)
+        .perform(mockUiController, targetView);
+    initInteraction();
+    try {
+      testInteraction.perform(mockAction);
+      fail("Should propagate exception stored in view operation!");
+    } catch (RuntimeException re) {
+      verify(mockAction).perform(mockUiController, targetView);
+      assertThat(exceptionToRaise, is(re));
+    }
+  }
+
+  public void testCheckPropagatesException() {
+    RuntimeException exceptionToRaise = new RuntimeException();
+    when(mockViewFinder.getView()).thenReturn(targetView);
+    doThrow(exceptionToRaise)
+      .when(mockAssertion)
+      .check(targetView, null);
+
+    initInteraction();
+    try {
+      testInteraction.check(mockAssertion);
+      fail("Should propagate exception stored in view operation!");
+    } catch (RuntimeException re) {
+      verify(mockAssertion).check(targetView, null);
+      assertThat(exceptionToRaise, is(re));
+    }
+  }
+
+  public void testPerformTwiceUpdatesPreviouslyMatched() {
+    View firstView = new View(getContext());
+    View secondView = new View(getContext());
+    when(mockViewFinder.getView()).thenReturn(firstView);
+    initInteraction();
+    testInteraction.perform(mockAction);
+    verify(mockAction).perform(mockUiController, firstView);
+
+    when(mockViewFinder.getView()).thenReturn(secondView);
+    testInteraction.perform(mockAction);
+    verify(mockAction).perform(mockUiController, secondView);
+
+    testInteraction.check(mockAssertion);
+    verify(mockAssertion).check(secondView, null);
+
+  }
+
+  public void testPerformAndCheck() {
+    when(mockViewFinder.getView()).thenReturn(targetView);
+    initInteraction();
+    testInteraction.perform(mockAction);
+    verify(mockAction).perform(mockUiController, targetView);
+
+    testInteraction.check(mockAssertion);
+    verify(mockAssertion).check(targetView, null);
+  }
+
+  public void testCheck() {
+    when(mockViewFinder.getView()).thenReturn(targetView);
+    initInteraction();
+    testInteraction.check(mockAssertion);
+    verify(mockAssertion).check(targetView, null);
+  }
+
+  public void testInRootUpdatesRef() {
+    initInteraction();
+    Matcher<Root> testMatcher = nullValue();
+    testInteraction.inRoot(testMatcher);
+    assertEquals(testMatcher, rootMatcherRef.get());
+  }
+
+  public void testInRoot_NullHandling() {
+    initInteraction();
+    try {
+      testInteraction.inRoot(null);
+      fail("should throw");
+    } catch (NullPointerException expected) {
+    }
+  }
+
+  public void testCheck_ViewCannotBeFound() {
+    NoMatchingViewException noViewException = new NoMatchingViewException.Builder()
+        .withViewMatcher(viewMatcher)
+        .withRootView(rootView)
+        .build();
+
+    when(mockViewFinder.getView()).thenThrow(noViewException);
+    initInteraction();
+    testInteraction.check(mockAssertion);
+    verify(mockAssertion).check(null, noViewException);
+  }
+
+  private void initInteraction() {
+    when(mockAction.getConstraints()).thenReturn(actionConstraint);
+
+    testInteraction = new ViewInteraction(mockUiController, mockViewFinder, testExecutor,
+        failureHandler, viewMatcher, rootMatcherRef);
+
+  }
+}
diff --git a/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/AdapterDataIntegrationTest.java b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/AdapterDataIntegrationTest.java
new file mode 100644
index 0000000..fe37fc5
--- /dev/null
+++ b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/AdapterDataIntegrationTest.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.action;
+
+import static com.google.android.apps.common.testing.ui.espresso.Espresso.onData;
+import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView;
+import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click;
+import static com.google.android.apps.common.testing.ui.espresso.assertion.ViewAssertions.matches;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.hasSibling;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withText;
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.hasEntry;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.is;
+
+import com.google.android.apps.common.testing.ui.testapp.LongListActivity;
+import com.google.android.apps.common.testing.ui.testapp.R;
+
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+
+import java.util.Map;
+
+/**
+ * Integration tests for operating on data displayed in an adapter.
+ */
+@LargeTest
+public class AdapterDataIntegrationTest extends ActivityInstrumentationTestCase2<LongListActivity> {
+  @SuppressWarnings("deprecation")
+  public AdapterDataIntegrationTest() {
+    // Supporting froyo.
+    super("com.google.android.apps.common.testing.ui.testapp", LongListActivity.class);
+  }
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    getActivity();
+  }
+
+  @SuppressWarnings("unchecked")
+  public void testClickAroundList() {
+    onData(allOf(is(instanceOf(Map.class)), hasEntry(is(LongListActivity.STR), is("item: 99"))))
+        .perform(click());
+    onView(withId(R.id.selection_row_value))
+        .check(matches(withText("99")));
+
+    onData(allOf(is(instanceOf(Map.class)), hasEntry(is(LongListActivity.STR), is("item: 1"))))
+        .perform(click());
+
+    onView(withId(R.id.selection_row_value))
+        .check(matches(withText("1")));
+
+    onData(allOf(is(instanceOf(Map.class))))
+        .atPosition(20)
+        .perform(click());
+
+    onView(withId(R.id.selection_row_value))
+        .check(matches(withText("20")));
+
+    // lets operate on a specific child of a row...
+    onData(allOf(is(instanceOf(Map.class)), hasEntry(is(LongListActivity.STR), is("item: 50"))))
+        .onChildView(withId(R.id.item_size))
+        .perform(click())
+        .check(matches(withText(String.valueOf("item: 50".length()))));
+
+    onView(withId(R.id.selection_row_value))
+        .check(matches(withText("50")));
+  }
+
+  @SuppressWarnings("unchecked")
+  public void testSelectItemWithSibling() {
+    onView(allOf(withText("7"), hasSibling(withText("item: 0"))))
+        .perform(click());
+    onView(withId(R.id.selection_row_value))
+        .check(matches(withText("0")));
+  }
+}
diff --git a/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/ClearTextActionIntegrationTest.java b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/ClearTextActionIntegrationTest.java
new file mode 100644
index 0000000..cf2835f
--- /dev/null
+++ b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/ClearTextActionIntegrationTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.action;
+
+import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView;
+import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.clearText;
+import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.typeText;
+import static com.google.android.apps.common.testing.ui.espresso.assertion.ViewAssertions.matches;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withText;
+import static org.hamcrest.Matchers.is;
+
+import com.google.android.apps.common.testing.ui.testapp.R;
+import com.google.android.apps.common.testing.ui.testapp.SendActivity;
+
+import android.app.Activity;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+
+/**
+ * {@link ClearTextAction} integration tests.
+ */
+@LargeTest
+public class ClearTextActionIntegrationTest extends ActivityInstrumentationTestCase2<SendActivity> {
+  @SuppressWarnings("deprecation")
+  public ClearTextActionIntegrationTest() {
+    // Supporting froyo.
+    super("com.google.android.apps.common.testing.ui.testapp", SendActivity.class);
+  }
+
+  @LargeTest
+  public void testClearTextActionPerform() {
+    Activity activity = getActivity();
+    String text = activity.getText(R.string.send_data_to_message_edit_text).toString();
+    onView(withId(is(R.id.send_data_to_message_edit_text))).check(matches(withText(is(text))));
+    onView(withId(is(R.id.send_data_to_message_edit_text))).perform(clearText());
+    onView(withId(is(R.id.send_data_to_message_edit_text))).check(matches(withText(is(""))));
+  }
+
+  @LargeTest
+  public void testClearTextActionPerformWithTypeText() {
+    Activity activity = getActivity();
+    String text = activity.getText(R.string.send_data_to_message_edit_text).toString();
+    onView(withId(is(R.id.send_data_to_call_edit_text))).perform(typeText(text));
+    onView(withId(is(R.id.send_data_to_call_edit_text))).check(matches(withText(is(text))));
+    onView(withId(is(R.id.send_data_to_call_edit_text))).perform(clearText());
+    onView(withId(is(R.id.send_data_to_call_edit_text))).check(matches(withText(is(""))));
+  }
+}
diff --git a/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/EditorActionIntegrationTest.java b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/EditorActionIntegrationTest.java
new file mode 100644
index 0000000..0a99be9
--- /dev/null
+++ b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/EditorActionIntegrationTest.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.action;
+
+import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView;
+import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.pressImeActionButton;
+import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.scrollTo;
+import static com.google.android.apps.common.testing.ui.espresso.assertion.ViewAssertions.matches;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.hasImeAction;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isDisplayed;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withText;
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.containsString;
+
+import com.google.android.apps.common.testing.ui.espresso.PerformException;
+import com.google.android.apps.common.testing.ui.testapp.R;
+import com.google.android.apps.common.testing.ui.testapp.SendActivity;
+
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.view.inputmethod.EditorInfo;
+
+/**
+ * Tests for {@link EditorAction}.
+ */
+@LargeTest
+public class EditorActionIntegrationTest extends ActivityInstrumentationTestCase2<SendActivity> {
+  @SuppressWarnings("deprecation")
+  public EditorActionIntegrationTest() {
+    // Supporting froyo.
+    super("com.google.android.apps.common.testing.ui.testapp", SendActivity.class);
+  }
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    getActivity();
+  }
+
+  @SuppressWarnings("unchecked")
+  public void testPressImeActionButtonOnSearchBox() {
+    String searchFor = "rainbows and unicorns";
+    onView(withId(R.id.search_box)).perform(scrollTo(), ViewActions.typeText(searchFor));
+    onView(withId(R.id.search_box))
+        .check(matches(hasImeAction(EditorInfo.IME_ACTION_SEARCH)))
+        .perform(pressImeActionButton());
+    onView(withId(R.id.search_result)).perform(scrollTo());
+    onView(withId(R.id.search_result))
+        .check(matches(allOf(isDisplayed(), withText(containsString(searchFor)))));
+  }
+
+  public void testPressImeActionButtonOnNonEditorWidget() {
+    try {
+      onView(withId(R.id.send_button)).perform(pressImeActionButton());
+      fail("Expected exception on previous call");
+    } catch (PerformException expected) {
+      assertTrue(expected.getCause() instanceof IllegalStateException);
+    }
+  }
+
+  public void testPressSearchOnDefaultEditText() {
+    onView(withId(R.id.enter_data_edit_text)).perform(pressImeActionButton());
+  }
+}
diff --git a/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/EspressoKeyBuilderTest.java b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/EspressoKeyBuilderTest.java
new file mode 100644
index 0000000..6df907d
--- /dev/null
+++ b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/EspressoKeyBuilderTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.action;
+
+import com.google.android.apps.common.testing.ui.espresso.action.EspressoKey.Builder;
+
+import android.os.Build;
+import android.view.KeyEvent;
+
+import junit.framework.TestCase;
+
+/**
+ * Unit tests for {@link Builder}.
+ */
+public class EspressoKeyBuilderTest extends TestCase {
+
+  static final int KEY_CODE = KeyEvent.KEYCODE_X;
+
+  public void testBuildWithNoMetaState() {
+    EspressoKey key = new Builder().withKeyCode(KEY_CODE).build();
+    assertEquals(KEY_CODE, key.getKeyCode());
+    assertEquals(0, key.getMetaState());
+  }
+
+  public void testBuildWithShiftPressed() {
+    EspressoKey key = new Builder().withKeyCode(KEY_CODE).withShiftPressed(true).build();
+    assertEquals(KEY_CODE, key.getKeyCode());
+    assertEquals(KeyEvent.META_SHIFT_ON, key.getMetaState());
+  }
+
+  public void testBuildWithCtrlPressed() {
+    EspressoKey key = new Builder().withKeyCode(KEY_CODE).withCtrlPressed(true).build();
+    assertEquals(KEY_CODE, key.getKeyCode());
+
+    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+      assertEquals(KeyEvent.META_CTRL_ON, key.getMetaState());
+    } else {
+      assertEquals(0, key.getMetaState());
+    }
+  }
+
+  public void testBuildWithAltPressed() {
+    EspressoKey key = new Builder().withKeyCode(KEY_CODE).withAltPressed(true).build();
+    assertEquals(KEY_CODE, key.getKeyCode());
+    assertEquals(KeyEvent.META_ALT_ON, key.getMetaState());
+  }
+
+  public void testBuildWithAllMetaKeysPressed() {
+    EspressoKey key = new Builder().withKeyCode(KEY_CODE)
+        .withShiftPressed(true)
+        .withCtrlPressed(true)
+        .withAltPressed(true)
+        .build();
+
+    assertEquals(KEY_CODE, key.getKeyCode());
+    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+      assertEquals(KeyEvent.META_SHIFT_ON | KeyEvent.META_CTRL_ON | KeyEvent.META_ALT_ON,
+          key.getMetaState());
+    } else {
+      assertEquals(KeyEvent.META_SHIFT_ON | KeyEvent.META_ALT_ON, key.getMetaState());
+    }
+  }
+}
\ No newline at end of file
diff --git a/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/EventActionIntegrationTest.java b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/EventActionIntegrationTest.java
new file mode 100644
index 0000000..fbf55ef
--- /dev/null
+++ b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/EventActionIntegrationTest.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.action;
+
+import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView;
+import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click;
+import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.doubleClick;
+import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.longClick;
+import static com.google.android.apps.common.testing.ui.espresso.assertion.ViewAssertions.matches;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isAssignableFrom;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isDisplayed;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withText;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+
+import com.google.android.apps.common.testing.testrunner.annotations.SdkSuppress;
+import com.google.android.apps.common.testing.ui.espresso.UiController;
+import com.google.android.apps.common.testing.ui.espresso.ViewAction;
+import com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers;
+import com.google.android.apps.common.testing.ui.testapp.GestureActivity;
+import com.google.android.apps.common.testing.ui.testapp.R;
+
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.view.View;
+
+import org.hamcrest.Matcher;
+
+/**
+ * UI tests for ClickAction, LongClickAction and DoubleClickAction.
+ */
+@LargeTest
+public class EventActionIntegrationTest extends ActivityInstrumentationTestCase2<GestureActivity> {
+
+  @SuppressWarnings("deprecation")
+  public EventActionIntegrationTest() {
+    // Keep froyo happy.
+    super("com.google.android.apps.common.testing.ui.testapp", GestureActivity.class);
+  }
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    getActivity();
+  }
+
+  public void testClick() {
+    onView(withText(is(getActivity().getString(R.string.text_click))))
+        .check(matches(not(isDisplayed())));
+    onView(withId(is(R.id.gesture_area))).perform(click());
+    onView(withId(is(R.id.text_click))).check(matches(isDisplayed()));
+    onView(withText(is(getActivity().getString(R.string.text_click))))
+        .check(matches(isDisplayed()));
+  }
+
+  public void testBadClick() {
+    onView(withText(is(getActivity().getString(R.string.text_click))))
+        .check(matches(not(isDisplayed())));
+    getActivity().setTouchDelay(700);
+
+    onView(withId(is(R.id.gesture_area))).perform(click(
+        new ViewAction() {
+          @Override
+          public String getDescription() {
+            return "Handle tap->longclick.";
+          }
+          @Override
+          public Matcher<View> getConstraints() {
+            return isAssignableFrom(View.class);
+          }
+          @Override
+          public void perform(UiController uiController, View view) {
+            getActivity().setTouchDelay(0);
+          }
+        }));
+
+
+    onView(withId(is(R.id.text_click))).check(matches(isDisplayed()));
+    onView(withText(is(getActivity().getString(R.string.text_click))))
+        .check(matches(isDisplayed()));
+  }
+
+  @SdkSuppress(bugId = -1, versions = {7, 8, 13})
+  public void testLongClick() {
+    onView(withText(is(getActivity().getString(R.string.text_long_click))))
+        .check(matches(not(isDisplayed())));
+    onView(withId(is(R.id.gesture_area))).perform(longClick());
+    onView(withId(is(R.id.text_long_click))).check(matches(isDisplayed()));
+    onView(withText(is(getActivity().getString(R.string.text_long_click))))
+        .check(matches(isDisplayed()));
+  }
+
+  @SdkSuppress(bugId = -1, versions = {7, 8, 13})
+  public void testDoubleClick() {
+    onView(withText(is(getActivity().getString(R.string.text_double_click))))
+        .check(matches(not(ViewMatchers.isDisplayed())));
+    onView(withId(is(R.id.gesture_area))).perform(doubleClick());
+    onView(withId(is(R.id.text_double_click))).check(matches(isDisplayed()));
+    onView(withText(is("Double Click"))).check(matches(isDisplayed()));
+    onView(withText(is(getActivity().getString(R.string.text_double_click))))
+        .check(matches(isDisplayed()));
+  }
+}
diff --git a/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/GeneralLocationTest.java b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/GeneralLocationTest.java
new file mode 100644
index 0000000..944e660
--- /dev/null
+++ b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/GeneralLocationTest.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.action;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.view.View;
+
+import junit.framework.TestCase;
+
+import org.mockito.Spy;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+/**
+ * Unit tests for {@link GeneralLocation}.
+ */
+public class GeneralLocationTest extends TestCase {
+
+  private static final int VIEW_POSITION_X = 100;
+  private static final int VIEW_POSITION_Y = 50;
+  private static final int VIEW_WIDTH = 150;
+  private static final int VIEW_HEIGHT = 300;
+
+  private static final int AXIS_X = 0;
+  private static final int AXIS_Y = 1;
+
+  @Spy
+  private View mockView;
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    initMocks(this);
+    
+    doAnswer(new Answer<Void>() {
+      @Override
+      public Void answer(InvocationOnMock invocation) throws Throwable {
+        int[] array = (int[]) invocation.getArguments()[0];
+        array[AXIS_X] = VIEW_POSITION_X;
+        array[AXIS_Y] = VIEW_POSITION_Y;
+        return null;
+      }
+    }).when(mockView).getLocationOnScreen(any(int[].class));
+
+    mockView.layout(
+            VIEW_POSITION_X,
+            VIEW_POSITION_Y,
+            VIEW_POSITION_X + VIEW_WIDTH,
+            VIEW_POSITION_Y + VIEW_HEIGHT);
+  }
+
+  public void testLeftLocationsX() {
+      assertPositionEquals(VIEW_POSITION_X, GeneralLocation.TOP_LEFT, AXIS_X);
+      assertPositionEquals(VIEW_POSITION_X, GeneralLocation.CENTER_LEFT, AXIS_X);
+      assertPositionEquals(VIEW_POSITION_X, GeneralLocation.BOTTOM_LEFT, AXIS_X);
+  }
+
+  public void testRightLocationsX() {
+    assertPositionEquals(VIEW_POSITION_X + VIEW_WIDTH, GeneralLocation.TOP_RIGHT, AXIS_X);
+    assertPositionEquals(VIEW_POSITION_X + VIEW_WIDTH, GeneralLocation.CENTER_RIGHT, AXIS_X);
+    assertPositionEquals(VIEW_POSITION_X + VIEW_WIDTH, GeneralLocation.BOTTOM_RIGHT, AXIS_X);
+  }
+
+  public void testTopLocationsY() {
+    assertPositionEquals(VIEW_POSITION_Y, GeneralLocation.TOP_LEFT, AXIS_Y);
+    assertPositionEquals(VIEW_POSITION_Y, GeneralLocation.TOP_CENTER, AXIS_Y);
+    assertPositionEquals(VIEW_POSITION_Y, GeneralLocation.TOP_RIGHT, AXIS_Y);
+  }
+
+  public void testBottomLocationsY() {
+    assertPositionEquals(VIEW_POSITION_Y + VIEW_HEIGHT, GeneralLocation.BOTTOM_LEFT, AXIS_Y);
+    assertPositionEquals(VIEW_POSITION_Y + VIEW_HEIGHT, GeneralLocation.BOTTOM_CENTER, AXIS_Y);
+    assertPositionEquals(VIEW_POSITION_Y + VIEW_HEIGHT, GeneralLocation.BOTTOM_RIGHT, AXIS_Y);
+  }
+
+  public void testCenterLocationsX() {
+    assertPositionEquals(VIEW_POSITION_X + VIEW_WIDTH / 2, GeneralLocation.CENTER, AXIS_X);
+    assertPositionEquals(VIEW_POSITION_X + VIEW_WIDTH / 2, GeneralLocation.TOP_CENTER, AXIS_X);
+    assertPositionEquals(VIEW_POSITION_X + VIEW_WIDTH / 2, GeneralLocation.BOTTOM_CENTER, AXIS_X);
+  }
+
+  public void testCenterLocationsY() {
+    assertPositionEquals(VIEW_POSITION_Y + VIEW_HEIGHT / 2, GeneralLocation.CENTER, AXIS_Y);
+    assertPositionEquals(VIEW_POSITION_Y + VIEW_HEIGHT / 2, GeneralLocation.CENTER_LEFT, AXIS_Y);
+    assertPositionEquals(VIEW_POSITION_Y + VIEW_HEIGHT / 2, GeneralLocation.CENTER_RIGHT, AXIS_Y);
+  }
+
+  private void assertPositionEquals(int expected, GeneralLocation location, int axis) {
+    assertEquals(expected, location.calculateCoordinates(mockView)[axis], 0.1f);
+  }
+
+}
diff --git a/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/KeyEventActionIntegrationTest.java b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/KeyEventActionIntegrationTest.java
new file mode 100644
index 0000000..c75c3fb
--- /dev/null
+++ b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/KeyEventActionIntegrationTest.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.action;
+
+import static com.google.android.apps.common.testing.ui.espresso.Espresso.onData;
+import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView;
+import static com.google.android.apps.common.testing.ui.espresso.Espresso.pressBack;
+import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click;
+import static com.google.android.apps.common.testing.ui.espresso.assertion.ViewAssertions.matches;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isAssignableFrom;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isDisplayed;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isRoot;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withParent;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withText;
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.hasValue;
+import static org.hamcrest.Matchers.instanceOf;
+
+import com.google.android.apps.common.testing.testrunner.annotations.SdkSuppress;
+import com.google.android.apps.common.testing.ui.espresso.NoActivityResumedException;
+import com.google.android.apps.common.testing.ui.testapp.MainActivity;
+import com.google.android.apps.common.testing.ui.testapp.R;
+
+import android.content.Intent;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.FlakyTest;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.view.KeyEvent;
+import android.widget.TextView;
+
+import java.util.Map;
+
+
+/**
+ * Integration tests for {@link KeyEventAction}.
+ */
+@LargeTest
+public class KeyEventActionIntegrationTest extends ActivityInstrumentationTestCase2<MainActivity> {
+  @SuppressWarnings("deprecation")
+  public KeyEventActionIntegrationTest() {
+    // Supporting froyo.
+    super("com.google.android.apps.common.testing.ui.testapp", MainActivity.class);
+  }
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+  }
+
+  public void testClickBackOnRootAction() {
+    getActivity();
+    try {
+      pressBack();
+      fail("Should have thrown NoActivityResumedException");
+    } catch (NoActivityResumedException expected) {
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  public void testClickBackOnNonRootActivityLatte() {
+    getActivity();
+    onData(allOf(instanceOf(Map.class), hasValue("SendActivity"))).perform(click());
+    pressBack();
+
+    // Make sure we are back.
+    onData(allOf(instanceOf(Map.class), hasValue("SendActivity"))).check(matches(isDisplayed()));
+  }
+
+  @SuppressWarnings("unchecked")
+  public void testClickBackOnNonRootActionNoLatte() {
+    getActivity();
+    onData(allOf(instanceOf(Map.class), hasValue("SendActivity"))).perform(click());
+    onView(isRoot()).perform(ViewActions.pressBack());
+
+    // Make sure we are back.
+    onData(allOf(instanceOf(Map.class), hasValue("SendActivity"))).check(matches(isDisplayed()));
+  }
+
+  @SuppressWarnings("unchecked")
+  @SdkSuppress(versions = {7, 8, 10}, bugId = -1) // uses native fragments.
+  @FlakyTest
+  public void testClickOnBackFromFragment() {
+    Intent fragmentStack = new Intent().setClassName(getInstrumentation().getTargetContext(),
+        "com.google.android.apps.common.testing.ui.testapp.FragmentStack");
+    fragmentStack.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+    getInstrumentation().startActivitySync(fragmentStack);
+    onView(allOf(withParent(withId(R.id.simple_fragment)), isAssignableFrom(TextView.class)))
+        .check(matches(withText(containsString("#1"))));
+    try {
+      pressBack();
+      fail("Should have thrown NoActivityResumedException");
+    } catch (NoActivityResumedException expected) {
+    }
+    getInstrumentation().startActivitySync(fragmentStack);
+
+    onView(withId(R.id.new_fragment)).perform(click()).perform(click()).perform(click());
+
+    onView(allOf(withParent(withId(R.id.simple_fragment)), isAssignableFrom(TextView.class)))
+        .check(matches(withText(containsString("#4"))));
+
+    pressBack();
+
+    onView(allOf(withParent(withId(R.id.simple_fragment)), isAssignableFrom(TextView.class)))
+        .check(matches(withText(containsString("#3"))));
+
+    pressBack();
+
+    onView(allOf(withParent(withId(R.id.simple_fragment)), isAssignableFrom(TextView.class)))
+        .check(matches(withText(containsString("#2"))));
+
+    pressBack();
+
+    onView(allOf(withParent(withId(R.id.simple_fragment)), isAssignableFrom(TextView.class)))
+        .check(matches(withText(containsString("#1"))));
+
+    try {
+      pressBack();
+      fail("Should have thrown NoActivityResumedException");
+    } catch (NoActivityResumedException expected) {
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  public void testPressKeyWithKeyCode() {
+    getActivity();
+    onData(allOf(instanceOf(Map.class), hasValue("SendActivity"))).perform(click());
+    onView(withId(R.id.enter_data_edit_text)).perform(click());
+    onView(withId(R.id.enter_data_edit_text)).perform(ViewActions.pressKey(KeyEvent.KEYCODE_X));
+    onView(withId(R.id.enter_data_edit_text)).perform(ViewActions.pressKey(KeyEvent.KEYCODE_Y));
+    onView(withId(R.id.enter_data_edit_text)).perform(ViewActions.pressKey(KeyEvent.KEYCODE_Z));
+    onView(withId(R.id.enter_data_edit_text)).perform(ViewActions.pressKey(KeyEvent.KEYCODE_ENTER));
+    onView(allOf(withId(R.id.enter_data_response_text), withText("xyz")))
+        .check(matches(isDisplayed()));
+  }
+}
diff --git a/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/ScrollToActionIntegrationTest.java b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/ScrollToActionIntegrationTest.java
new file mode 100644
index 0000000..60ca48b
--- /dev/null
+++ b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/ScrollToActionIntegrationTest.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.action;
+
+import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView;
+import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.scrollTo;
+import static com.google.android.apps.common.testing.ui.espresso.assertion.ViewAssertions.matches;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isDisplayed;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+
+import com.google.android.apps.common.testing.ui.testapp.R;
+import com.google.android.apps.common.testing.ui.testapp.ScrollActivity;
+
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+
+/**
+ * Tests for ScrollToAction.
+ */
+@LargeTest
+public class ScrollToActionIntegrationTest extends ActivityInstrumentationTestCase2<ScrollActivity>
+{
+  @SuppressWarnings("deprecation")
+  public ScrollToActionIntegrationTest() {
+    // Keep froyo happy.
+    super("com.google.android.apps.common.testing.ui.testapp", ScrollActivity.class);
+  }
+
+  @Override
+  protected void setUp() throws Exception {
+    super.setUp();
+    getActivity();
+  }
+
+  public void testScrollDown() {
+    onView(withId(is(R.id.bottom_left)))
+      .check(matches(not(isDisplayed())))
+      .perform(scrollTo())
+      .check(matches(isDisplayed()))
+      .perform(scrollTo()); // Should be a noop.
+  }
+
+  public void testScrollVerticalAndHorizontal() {
+    onView(withId(is(R.id.bottom_right)))
+      .check(matches(not(isDisplayed())))
+      .perform(scrollTo())
+      .check(matches(isDisplayed()));
+    onView(withId(is(R.id.top_left)))
+      .check(matches(not(isDisplayed())))
+      .perform(scrollTo())
+      .check(matches(isDisplayed()));
+  }
+
+  public void testScrollWithinScroll() {
+    onView(withId(is(R.id.double_scroll)))
+      .check(matches(not(isDisplayed())))
+      .perform(scrollTo())
+      .check(matches(isDisplayed()));
+  }
+}
diff --git a/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/SwipeActionIntegrationTest.java b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/SwipeActionIntegrationTest.java
new file mode 100644
index 0000000..90257bc
--- /dev/null
+++ b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/SwipeActionIntegrationTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.action;
+
+import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView;
+import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.swipeLeft;
+import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.swipeRight;
+import static com.google.android.apps.common.testing.ui.espresso.assertion.ViewAssertions.matches;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.hasDescendant;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isDisplayed;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withText;
+import static org.hamcrest.Matchers.allOf;
+
+import com.google.android.apps.common.testing.ui.testapp.R;
+import com.google.android.apps.common.testing.ui.testapp.SwipeActivity;
+
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+
+/**
+ * Integration tests for swiping actions.
+ */
+@LargeTest
+public class SwipeActionIntegrationTest extends ActivityInstrumentationTestCase2<SwipeActivity> {
+
+  @SuppressWarnings("deprecation")
+  public SwipeActionIntegrationTest() {
+    // Keep froyo happy.
+    super("com.google.android.apps.common.testing.ui.testapp", SwipeActivity.class);
+  }
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    getActivity();
+  }
+
+  /** Tests that a small view can be swiped in both directions. */
+  public void testSwipeOverSmallView() {
+    onView(withId(R.id.small_pager))
+      .check(matches(hasDescendant(withText("Position #0"))))
+      .perform(swipeLeft())
+      .check(matches(hasDescendant(withText("Position #1"))))
+      .perform(swipeLeft())
+      .check(matches(hasDescendant(withText("Position #2"))))
+      .perform(swipeRight())
+      .check(matches(hasDescendant(withText("Position #1"))))
+      .perform(swipeRight())
+      .check(matches(hasDescendant(withText("Position #0"))));
+  }
+
+  /** Tests that trying to swipe beyond the start of a view pager has no effect. */
+  public void testSwipingRightHasNoEffectWhenAtStart() {
+    onView(withId(R.id.small_pager))
+      .check(matches(hasDescendant(withText("Position #0"))))
+      .perform(swipeRight())
+      .check(matches(hasDescendant(withText("Position #0"))))
+      .perform(swipeRight())
+      .check(matches(hasDescendant(withText("Position #0"))));
+  }
+
+  /** Tests that trying to swipe beyond the end of a view pager has no effect. */
+  public void testSwipingLeftHasNoEffectWhenAtEnd() {
+    onView(withId(R.id.small_pager))
+      .perform(swipeLeft())
+      .perform(swipeLeft())
+      .check(matches(hasDescendant(withText("Position #2"))))
+      .perform(swipeLeft())
+      .check(matches(hasDescendant(withText("Position #2"))))
+      .perform(swipeLeft())
+      .check(matches(hasDescendant(withText("Position #2"))));
+  }
+
+  /** Tests that swiping across a partially overlapped view works correctly. */
+  public void testSwipeOverPartiallyOverlappedView() {
+    onView(withId(R.id.overlapped_pager))
+      .check(matches(hasDescendant(withText("Position #0"))))
+      .perform(swipeLeft())
+      .check(matches(hasDescendant(withText("Position #1"))))
+      .perform(swipeRight())
+      .check(matches(hasDescendant(withText("Position #0"))));
+  }
+
+  /** Tests that trying to swipe a view that doesn't respond to swipes has no effect. */
+  @SuppressWarnings("unchecked")
+  public void testSwipeOverUnswipableView() {
+    onView(withId(R.id.text_simple))
+      .check(matches(allOf(isDisplayed(), withText(R.string.text_simple))))
+      .perform(swipeLeft())
+      .check(matches(allOf(isDisplayed(), withText(R.string.text_simple))))
+      .perform(swipeRight())
+      .check(matches(allOf(isDisplayed(), withText(R.string.text_simple))));
+  }
+
+}
diff --git a/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/TypeTextActionIntegrationTest.java b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/TypeTextActionIntegrationTest.java
new file mode 100644
index 0000000..b1130f1
--- /dev/null
+++ b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/TypeTextActionIntegrationTest.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.action;
+
+import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView;
+import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.pressImeActionButton;
+import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.scrollTo;
+import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.typeText;
+import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.typeTextIntoFocusedView;
+import static com.google.android.apps.common.testing.ui.espresso.assertion.ViewAssertions.matches;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isDisplayed;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withParent;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withText;
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+
+import com.google.android.apps.common.testing.ui.espresso.PerformException;
+import com.google.android.apps.common.testing.ui.testapp.R;
+import com.google.android.apps.common.testing.ui.testapp.SendActivity;
+
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+
+/**
+ * {@link TypeTextAction} integration tests.
+ */
+@LargeTest
+public class TypeTextActionIntegrationTest extends ActivityInstrumentationTestCase2<SendActivity> {
+  @SuppressWarnings("deprecation")
+  public TypeTextActionIntegrationTest() {
+    // Supporting froyo.
+    super("com.google.android.apps.common.testing.ui.testapp", SendActivity.class);
+  }
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    getActivity();
+  }
+
+  public void testTypeTextActionPerform() {
+    onView(withId(is(R.id.send_data_to_call_edit_text))).perform(typeText("Hello!"));
+  }
+
+  @SuppressWarnings("unchecked")
+  public void testTypeTextActionPerformWithEnter() {
+    onView(withId(R.id.enter_data_edit_text)).perform(typeText("Hello World!\n"));
+    onView(allOf(withId(R.id.enter_data_response_text), withText("Hello World!")))
+        .check(matches(isDisplayed()));
+  }
+
+  public void testTypeTextInFocusedView() {
+    onView(withId(is(R.id.send_data_to_call_edit_text))).perform(typeText(
+        "Hello World How Are You Today? I have alot of text to type."));
+    onView(withId(is(R.id.send_data_to_call_edit_text))).perform(typeTextIntoFocusedView(
+        "Jolly good!"));
+    onView(withId(is(R.id.send_data_to_call_edit_text))).check(matches(withText(
+        "Hello World How Are You Today? I have alot of text to type.Jolly good!")));
+  }
+
+  public void testTypeTextInFocusedView_constraintBreakage() {
+    onView(withId(is(R.id.send_data_to_call_edit_text))).perform(typeText(
+        "Hello World How Are You Today? I have alot of text to type."));
+    try {
+      onView(withId(is(R.id.edit_text_message)))
+          .perform(scrollTo(), typeTextIntoFocusedView("Jolly good!"));
+      fail("Should not have been able to type into focused view.");
+    } catch (PerformException expected) {
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  public void testTypeTextInDelegatedEditText() {
+    String toType = "honeybadger doesn't care";
+    onView(allOf(withParent(withId(R.id.delegating_edit_text)), withId(R.id.delegate_edit_text)))
+        .perform(scrollTo(), typeText(toType), pressImeActionButton());
+    onView(withId(R.id.edit_text_message))
+      .perform(scrollTo())
+      .check(matches(withText(containsString(toType))));
+  }
+}
diff --git a/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/TypeTextActionTest.java b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/TypeTextActionTest.java
new file mode 100644
index 0000000..acddc06
--- /dev/null
+++ b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/TypeTextActionTest.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.action;
+
+import static org.mockito.Matchers.isA;
+import static org.mockito.Mockito.when;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import com.google.android.apps.common.testing.ui.espresso.InjectEventSecurityException;
+import com.google.android.apps.common.testing.ui.espresso.PerformException;
+import com.google.android.apps.common.testing.ui.espresso.UiController;
+
+import android.view.MotionEvent;
+import android.view.View;
+
+import junit.framework.TestCase;
+
+import org.mockito.Mock;
+
+/**
+ * Unit tests for {@link TypeTextAction}.
+ */
+public class TypeTextActionTest extends TestCase {
+  @Mock
+  private UiController mockUiController;
+
+  @Mock
+  private View mockView;
+
+  private TypeTextAction typeTextAction;
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    initMocks(this);
+  }
+
+  public void testTypeTextActionPerform() throws InjectEventSecurityException {
+    String stringToBeTyped = "Hello!";
+    typeTextAction = new TypeTextAction(stringToBeTyped);
+    when(mockUiController.injectMotionEvent(isA(MotionEvent.class))).thenReturn(true);
+    when(mockUiController.injectString(stringToBeTyped)).thenReturn(true);
+    typeTextAction.perform(mockUiController, mockView);
+  }
+
+  public void testTypeTextActionPerformFailed() throws InjectEventSecurityException {
+    String stringToBeTyped = "Hello!";
+    typeTextAction = new TypeTextAction(stringToBeTyped);
+    when(mockUiController.injectMotionEvent(isA(MotionEvent.class))).thenReturn(true);
+    when(mockUiController.injectString(stringToBeTyped)).thenReturn(false);
+
+    try {
+      typeTextAction.perform(mockUiController, mockView);
+      fail("Should have thrown PerformException");
+    } catch (PerformException e) {
+      if (e.getCause() instanceof InjectEventSecurityException) {
+        fail("Exception cause should NOT be of type InjectEventSecurityException");
+      }
+    }
+  }
+
+  public void testTypeTextActionPerformInjectEventSecurityException()
+      throws InjectEventSecurityException {
+    String stringToBeTyped = "Hello!";
+    typeTextAction = new TypeTextAction(stringToBeTyped);
+    when(mockUiController.injectMotionEvent(isA(MotionEvent.class))).thenReturn(true);
+    when(mockUiController.injectString(stringToBeTyped))
+        .thenThrow(new InjectEventSecurityException(""));
+
+    try {
+      typeTextAction.perform(mockUiController, mockView);
+      fail("Should have thrown PerformException");
+    } catch (PerformException e) {
+      if (!(e.getCause() instanceof InjectEventSecurityException)) {
+        fail("Exception cause should be of type InjectEventSecurityException");
+      }
+    }
+  }
+}
diff --git a/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/WindowOrderingIntegrationTest.java b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/WindowOrderingIntegrationTest.java
new file mode 100644
index 0000000..d50e409
--- /dev/null
+++ b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/WindowOrderingIntegrationTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.action;
+
+import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView;
+import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click;
+import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.scrollTo;
+import static com.google.android.apps.common.testing.ui.espresso.assertion.ViewAssertions.doesNotExist;
+import static com.google.android.apps.common.testing.ui.espresso.assertion.ViewAssertions.matches;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isDisplayed;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withText;
+
+import com.google.android.apps.common.testing.ui.testapp.R;
+import com.google.android.apps.common.testing.ui.testapp.SendActivity;
+
+import android.os.Build;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+
+/**
+ * Ensures view root ordering works properly.
+ */
+@LargeTest
+public class WindowOrderingIntegrationTest extends ActivityInstrumentationTestCase2<SendActivity> {
+  @SuppressWarnings("deprecation")
+  public WindowOrderingIntegrationTest() {
+    // Supporting froyo.
+    super("com.google.android.apps.common.testing.ui.testapp", SendActivity.class);
+  }
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    getActivity();
+  }
+
+  public void testPopupMenu() {
+    if (Build.VERSION.SDK_INT < 11) {
+      // popup menus are post honeycomb.
+      return;
+    }
+    onView(withText(R.string.item_1_text))
+        .check(doesNotExist());
+    onView(withId(R.id.make_popup_menu_button))
+        .perform(scrollTo(), click());
+    onView(withText(R.string.item_1_text))
+        .check(matches(isDisplayed()))
+        .perform(click());
+    onView(withText(R.string.item_1_text))
+        .check(doesNotExist());
+  }
+
+  public void testPopupWindow() {
+    getActivity();
+    onView(withId(R.id.popup_title))
+        .check(doesNotExist());
+    onView(withId(R.id.make_popup_view_button))
+        .perform(scrollTo(), click());
+    onView(withId(R.id.popup_title))
+        .check(matches(withText(R.string.popup_title)))
+        .perform(click());
+    onView(withId(R.id.popup_title))
+        .check(doesNotExist());
+  }
+
+  public void testDialog() {
+    onView(withText(R.string.dialog_title))
+        .check(doesNotExist());
+    onView(withId(R.id.make_alert_dialog))
+        .perform(scrollTo(), click());
+    onView(withText(R.string.dialog_title))
+        .check(matches(isDisplayed()));
+
+    onView(withText("Fine"))
+        .perform(click());
+
+    onView(withText(R.string.dialog_title))
+        .check(doesNotExist());
+  }
+}
diff --git a/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/assertion/ViewAssertionsTest.java b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/assertion/ViewAssertionsTest.java
new file mode 100644
index 0000000..b49a822
--- /dev/null
+++ b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/assertion/ViewAssertionsTest.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.assertion;
+
+import static com.google.android.apps.common.testing.ui.espresso.assertion.ViewAssertions.matches;
+import static com.google.android.apps.common.testing.ui.espresso.assertion.ViewAssertions.selectedDescendantsMatch;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.hasContentDescription;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isAssignableFrom;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withText;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.nullValue;
+
+import com.google.android.apps.common.testing.ui.espresso.NoMatchingViewException;
+
+import android.test.InstrumentationTestCase;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.RelativeLayout;
+import android.widget.ScrollView;
+import android.widget.TextView;
+
+import junit.framework.AssertionFailedError;
+
+import org.hamcrest.Matcher;
+
+/**
+ * Unit tests for {@link ViewAssertions}.
+ */
+public class ViewAssertionsTest extends InstrumentationTestCase {
+
+  private View presentView;
+  private View absentView;
+  private NoMatchingViewException absentException;
+  private NoMatchingViewException presentException;
+  private Matcher<View> alwaysAccepts;
+  private Matcher<View> alwaysFails;
+  private Matcher<View> nullViewMatcher;
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    presentView = new View(getInstrumentation().getTargetContext());
+    absentView = null;
+    absentException = null;
+    alwaysAccepts = is(presentView);
+    alwaysFails = not(is(presentView));
+    nullViewMatcher = nullValue();
+
+    presentException = new NoMatchingViewException.Builder()
+        .withViewMatcher(alwaysFails)
+        .withRootView(new View(getInstrumentation().getTargetContext()))
+        .build();
+  }
+
+  public void testViewPresent_MatcherFail() {
+    try {
+      matches(alwaysFails).check(presentView, absentException);
+    } catch (AssertionFailedError expected) {
+      return;
+    }
+    // cannot place inside try block, would be caught.
+    fail("Should not accept.");
+  }
+
+  public void testViewPresent_MatcherPass() {
+    try {
+      matches(alwaysAccepts).check(presentView, absentException);
+    } catch (AssertionError error) {
+      throw new RuntimeException("Should not die!!!", error);
+    }
+  }
+
+  public void testViewAbsent_Unexpectedly() {
+    try {
+      matches(alwaysAccepts).check(absentView, presentException);
+    } catch (NoMatchingViewException expected) {
+      return;
+    }
+
+    fail("should not accept, view not present.");
+  }
+
+  public void testViewAbsent_AndThatsWhatIWant() {
+    try {
+      matches(nullViewMatcher).check(absentView, presentException);
+    } catch (NoMatchingViewException expected) {
+      return;
+    }
+
+    fail("should not accept, view not present.");
+  }
+
+  public void testSelectedDescendantsMatch_ThereAreNone() {
+    View grany = setUpViewHierarchy();
+
+    try {
+      selectedDescendantsMatch(withText("welfjkw"), hasContentDescription())
+          .check(grany, absentException);
+    } catch (AssertionError error) {
+      throw new RuntimeException("Should not die!!!", error);
+    }
+  }
+
+  public void testSelectedDescendantsMatch_SelectedDescendantsMatch() {
+    View grany = setUpViewHierarchy();
+
+    try {
+      selectedDescendantsMatch(withText("has content description"), hasContentDescription())
+          .check(grany, absentException);
+    } catch (AssertionError error) {
+      throw new RuntimeException("Should not die!!!", error);
+    }
+  }
+
+  public void testSelectedDescendantsMatch_SelectedDescendantsDoNotMatch() {
+    View grany = setUpViewHierarchy();
+
+    try {
+      selectedDescendantsMatch(withText("no content description"), hasContentDescription())
+          .check(grany, absentException);
+    } catch (AssertionFailedError expected) {
+      return;
+    }
+
+    fail("should fail because descendants do not match.");
+  }
+
+  public void testSelectedDescendantsMatch_SelectedDescendantsMatchAndDoNotMatch() {
+    View grany = setUpViewHierarchy();
+
+    try {
+      selectedDescendantsMatch(isAssignableFrom(TextView.class), hasContentDescription())
+          .check(grany, absentException);
+    } catch (AssertionFailedError expected) {
+      return;
+    }
+
+    fail("should fail because not all descendants match.");
+  }
+
+  private View setUpViewHierarchy() {
+    TextView v1 = new TextView(getInstrumentation().getTargetContext());
+    v1.setText("no content description");
+    TextView v2 = new TextView(getInstrumentation().getTargetContext());
+    v2.setText("has content description");
+    v2.setContentDescription("content description");
+    ViewGroup parent = new RelativeLayout(getInstrumentation().getTargetContext());
+    View grany = new ScrollView(getInstrumentation().getTargetContext());
+    ((ViewGroup) grany).addView(parent);
+    parent.addView(v1);
+    parent.addView(v2);
+
+    return grany;
+  }
+}
diff --git a/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/base/AsyncTaskPoolMonitorTest.java b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/base/AsyncTaskPoolMonitorTest.java
new file mode 100644
index 0000000..f400cf0
--- /dev/null
+++ b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/base/AsyncTaskPoolMonitorTest.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.base;
+
+import junit.framework.TestCase;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Unit test for {@link AsyncTaskPoolMonitor}
+ */
+public class AsyncTaskPoolMonitorTest extends TestCase {
+
+  private final ThreadPoolExecutor testThreadPool = new ThreadPoolExecutor(
+      4, 4, 1, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
+
+  private AsyncTaskPoolMonitor monitor = new AsyncTaskPoolMonitor(testThreadPool);
+
+  @Override
+  public void tearDown() throws Exception {
+    testThreadPool.shutdownNow();
+    super.tearDown();
+  }
+
+  public void testIsIdle_onEmptyPool() throws Exception {
+    assertTrue(monitor.isIdleNow());
+    final AtomicBoolean isIdle = new AtomicBoolean(false);
+    // since we're already idle, this should be ran immedately on our thread.
+    monitor.notifyWhenIdle(new Runnable() {
+      @Override
+      public void run() {
+        isIdle.set(true);
+      }
+    });
+    assertTrue(isIdle.get());
+  }
+
+  public void testIsIdle_withRunningTask() throws Exception {
+    final CountDownLatch runLatch = new CountDownLatch(1);
+    testThreadPool.submit(new Runnable() {
+      @Override
+      public void run() {
+        runLatch.countDown();
+        try {
+          Thread.sleep(50000);
+        } catch (InterruptedException ie) {
+          throw new RuntimeException(ie);
+        }
+      }
+    });
+    assertTrue(runLatch.await(1, TimeUnit.SECONDS));
+    assertFalse(monitor.isIdleNow());
+
+    final AtomicBoolean isIdle = new AtomicBoolean(false);
+    monitor.notifyWhenIdle(new Runnable() {
+      @Override
+      public void run() {
+        isIdle.set(true);
+      }
+    });
+    // runnable shouldn't be run ever..
+    assertFalse(isIdle.get());
+  }
+
+
+  public void testIdleNotificationAndRestart() throws Exception {
+
+    FutureTask<Thread> workerThreadFetchTask = new FutureTask<Thread>(new Callable<Thread>() {
+      @Override
+      public Thread call() {
+        return Thread.currentThread();
+      }
+    });
+    testThreadPool.submit(workerThreadFetchTask);
+
+    Thread workerThread = workerThreadFetchTask.get();
+
+    final CountDownLatch runLatch = new CountDownLatch(1);
+    final CountDownLatch exitLatch = new CountDownLatch(1);
+
+    testThreadPool.submit(new Runnable() {
+      @Override
+      public void run() {
+        runLatch.countDown();
+        try {
+          exitLatch.await();
+        } catch (InterruptedException ie) {
+          throw new RuntimeException(ie);
+        }
+      }
+    });
+
+    assertTrue(runLatch.await(1, TimeUnit.SECONDS));
+    final CountDownLatch notificationLatch = new CountDownLatch(1);
+    monitor.notifyWhenIdle(new Runnable() {
+      @Override
+      public void run() {
+        notificationLatch.countDown();
+      }
+    });
+    // give some time for the idle detection threads to spin up.
+    Thread.sleep(2000);
+    // interrupt one of them
+    workerThread.interrupt();
+    Thread.sleep(1000);
+    // unblock the dummy work item.
+    exitLatch.countDown();
+    assertTrue(notificationLatch.await(1, TimeUnit.SECONDS));
+    assertTrue(monitor.isIdleNow());
+  }
+
+  public void testIdleNotification_extraWork() throws Exception {
+    final CountDownLatch firstRunLatch = new CountDownLatch(1);
+    final CountDownLatch firstExitLatch = new CountDownLatch(1);
+
+    testThreadPool.submit(new Runnable() {
+      @Override
+      public void run() {
+        firstRunLatch.countDown();
+        try {
+          firstExitLatch.await();
+        } catch (InterruptedException ie) {
+          throw new RuntimeException(ie);
+        }
+      }
+    });
+
+    assertTrue(firstRunLatch.await(1, TimeUnit.SECONDS));
+
+    final CountDownLatch notificationLatch = new CountDownLatch(1);
+    monitor.notifyWhenIdle(new Runnable() {
+      @Override
+      public void run() {
+        notificationLatch.countDown();
+      }
+    });
+
+    final CountDownLatch secondRunLatch = new CountDownLatch(1);
+    final CountDownLatch secondExitLatch = new CountDownLatch(1);
+    testThreadPool.submit(new Runnable() {
+      @Override
+      public void run() {
+        secondRunLatch.countDown();
+        try {
+          secondExitLatch.await();
+        } catch (InterruptedException ie) {
+          throw new RuntimeException(ie);
+        }
+      }
+    });
+
+    assertFalse(notificationLatch.await(10, TimeUnit.MILLISECONDS));
+    firstExitLatch.countDown();
+    assertFalse(notificationLatch.await(500, TimeUnit.MILLISECONDS));
+    secondExitLatch.countDown();
+    assertTrue(notificationLatch.await(1, TimeUnit.SECONDS));
+    assertTrue(monitor.isIdleNow());
+  }
+}
diff --git a/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/base/DefaultFailureHandlerTest.java b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/base/DefaultFailureHandlerTest.java
new file mode 100644
index 0000000..b84584c
--- /dev/null
+++ b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/base/DefaultFailureHandlerTest.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.base;
+
+import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView;
+import static com.google.android.apps.common.testing.ui.espresso.assertion.ViewAssertions.matches;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isDisplayed;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isRoot;
+import static com.google.common.base.Throwables.getStackTraceAsString;
+import static org.hamcrest.Matchers.not;
+
+import com.google.android.apps.common.testing.ui.espresso.AmbiguousViewMatcherException;
+import com.google.android.apps.common.testing.ui.espresso.NoMatchingViewException;
+import com.google.android.apps.common.testing.ui.espresso.ViewAssertion;
+import com.google.android.apps.common.testing.ui.testapp.MainActivity;
+
+import android.test.ActivityInstrumentationTestCase2;
+import android.view.View;
+
+import junit.framework.AssertionFailedError;
+
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+
+/**
+ * Tests Espresso's default failure handling.
+ */
+public class DefaultFailureHandlerTest extends ActivityInstrumentationTestCase2<MainActivity> {
+
+  @SuppressWarnings("deprecation")
+  public DefaultFailureHandlerTest() {
+    // Supporting froyo.
+    super("com.google.android.apps.common.testing.ui.testapp", MainActivity.class);
+  }
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    getActivity();
+  }
+
+  public void testMismatchInCheck() {
+    try {
+      onView(isRoot()).check(matches(not(isDisplayed())));
+      fail("Previous call expected to fail");
+    } catch (AssertionFailedError e) {
+      assertFailureStackContainsThisClass(e);
+    }
+  }
+
+  public void testCustomAssertionError() {
+    try {
+      onView(isRoot()).check(new ViewAssertion() {
+        @Override
+        public void check(View view, NoMatchingViewException noViewFoundException) {
+          assertFalse(true);
+        }
+      });
+      fail("Previous call expected to fail");
+    } catch (AssertionFailedError e) {
+      assertFailureStackContainsThisClass(e);
+    }
+  }
+
+  public void testNoMatchingViewException() {
+    try {
+      onView(withMatchesThatReturns(false)).check(matches(not(isDisplayed())));
+      fail("Previous call expected to fail");
+    } catch (NoMatchingViewException e) {
+      assertFailureStackContainsThisClass(e);
+    }
+  }
+
+  public void testAmbiguousViewMatcherException() {
+    try {
+      onView(withMatchesThatReturns(true)).check(matches(isDisplayed()));
+    } catch (RuntimeException e) {
+      assertTrue(e instanceof AmbiguousViewMatcherException);
+      assertFailureStackContainsThisClass(e);
+    }
+  }
+
+  private void assertFailureStackContainsThisClass(Throwable e) {
+    assertTrue(getStackTraceAsString(e).contains(getClass().getSimpleName().toString()));
+  }
+
+  private static Matcher<View> withMatchesThatReturns(final boolean returnValue) {
+    return new BaseMatcher<View>() {
+      @Override
+      public void describeTo(Description description) {
+        description.appendText("matches=" + returnValue);
+      }
+
+      @Override
+      public boolean matches(Object item) {
+        return returnValue;
+      }
+    };
+  }
+
+}
diff --git a/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/base/EventInjectorTest.java b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/base/EventInjectorTest.java
new file mode 100644
index 0000000..bbc367a
--- /dev/null
+++ b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/base/EventInjectorTest.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.base;
+
+import com.google.android.apps.common.testing.testrunner.ActivityLifecycleCallback;
+import com.google.android.apps.common.testing.testrunner.ActivityLifecycleMonitorRegistry;
+import com.google.android.apps.common.testing.testrunner.Stage;
+import com.google.android.apps.common.testing.ui.espresso.InjectEventSecurityException;
+import com.google.android.apps.common.testing.ui.testapp.R;
+import com.google.android.apps.common.testing.ui.testapp.SendActivity;
+
+import android.app.Activity;
+import android.os.Build;
+import android.os.SystemClock;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Tests for {@link EventInjector}.
+ */
+public class EventInjectorTest extends ActivityInstrumentationTestCase2<SendActivity> {
+  private static final String TAG = EventInjectorTest.class.getSimpleName();
+  private Activity sendActivity;
+  private EventInjector injector;
+  final AtomicBoolean injectEventWorked = new AtomicBoolean(false);
+  final AtomicBoolean injectEventThrewSecurityException = new AtomicBoolean(false);
+  final CountDownLatch latch = new CountDownLatch(1);
+
+  @SuppressWarnings("deprecation")
+  public EventInjectorTest() {
+    // Supporting froyo.
+    super("com.google.android.apps.common.testing.ui.testapp", SendActivity.class);
+  }
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    if (Build.VERSION.SDK_INT > 15) {
+      InputManagerEventInjectionStrategy strat = new InputManagerEventInjectionStrategy();
+      strat.initialize();
+      injector = new EventInjector(strat);
+    } else {
+      WindowManagerEventInjectionStrategy strat = new WindowManagerEventInjectionStrategy();
+      strat.initialize();
+      injector = new EventInjector(strat);
+    }
+  }
+
+  @Override
+  public void tearDown() throws Exception {
+    super.tearDown();
+  }
+
+  @LargeTest
+  public void testInjectKeyEventUpWithNoDown() throws Exception {
+    sendActivity = getActivity();
+
+    getInstrumentation().runOnMainSync(new Runnable() {
+      @Override
+      public void run() {
+        View view = sendActivity.findViewById(R.id.send_data_edit_text);
+        assertTrue(view.requestFocus());
+        latch.countDown();
+      }
+    });
+
+    assertTrue("Timed out!", latch.await(10, TimeUnit.SECONDS));
+    KeyCharacterMap keyCharacterMap = UiControllerImpl.getKeyCharacterMap();
+    KeyEvent[] events = keyCharacterMap.getEvents("a".toCharArray());
+    assertTrue(injector.injectKeyEvent(events[1]));
+  }
+
+  @LargeTest
+  public void testInjectStaleKeyEvent() throws Exception {
+    sendActivity = getActivity();
+
+    getInstrumentation().runOnMainSync(new Runnable() {
+      @Override
+      public void run() {
+        View view = sendActivity.findViewById(R.id.send_data_edit_text);
+        assertTrue(view.requestFocus());
+        latch.countDown();
+      }
+    });
+
+    assertTrue("Timed out!", latch.await(10, TimeUnit.SECONDS));
+    assertFalse("SecurityException exception was thrown.", injectEventThrewSecurityException.get());
+
+    KeyCharacterMap keyCharacterMap = UiControllerImpl.getKeyCharacterMap();
+    KeyEvent[] events = keyCharacterMap.getEvents("a".toCharArray());
+    KeyEvent event = KeyEvent.changeTimeRepeat(events[0], 1, 0);
+
+    // Stale event does not fail for API < 13.
+    if (Build.VERSION.SDK_INT < 13) {
+      assertTrue(injector.injectKeyEvent(event));
+    } else {
+      assertFalse(injector.injectKeyEvent(event));
+    }
+  }
+
+  @LargeTest
+  public void testInjectKeyEvent_securityException() {
+    KeyCharacterMap keyCharacterMap = UiControllerImpl.getKeyCharacterMap();
+    KeyEvent[] events = keyCharacterMap.getEvents("a".toCharArray());
+    try {
+      injector.injectKeyEvent(events[0]);
+      fail("Should have thrown a security exception!");
+    } catch (InjectEventSecurityException expected) { }
+  }
+
+  @LargeTest
+  public void testInjectMotionEvent_securityException() throws Exception {
+    getInstrumentation().runOnMainSync(new Runnable() {
+      @Override
+      public void run() {
+        MotionEvent down = MotionEvent.obtain(SystemClock.uptimeMillis(),
+            SystemClock.uptimeMillis(),
+            MotionEvent.ACTION_DOWN,
+            0,
+            0,
+            0);
+        try {
+          injector.injectMotionEvent(down);
+        } catch (InjectEventSecurityException expected) {
+          injectEventThrewSecurityException.set(true);
+        }
+        latch.countDown();
+      }
+    });
+
+    latch.await(10, TimeUnit.SECONDS);
+    assertTrue(injectEventThrewSecurityException.get());
+  }
+
+  @LargeTest
+  public void testInjectMotionEvent_upEventFailure() throws InterruptedException {
+    final CountDownLatch activityStarted = new CountDownLatch(1);
+    ActivityLifecycleCallback callback = new ActivityLifecycleCallback() {
+      @Override
+      public void onActivityLifecycleChanged(Activity activity, Stage stage) {
+        if (Stage.RESUMED == stage && activity instanceof SendActivity) {
+          activityStarted.countDown();
+        }
+      }
+    };
+    ActivityLifecycleMonitorRegistry
+        .getInstance()
+        .addLifecycleCallback(callback);
+    try {
+      getActivity();
+      assertTrue(activityStarted.await(20, TimeUnit.SECONDS));
+      final int[] xy = UiControllerImplIntegrationTest.getCoordinatesInMiddleOfSendButton(
+          getActivity(), getInstrumentation());
+
+      getInstrumentation().runOnMainSync(new Runnable() {
+        @Override
+        public void run() {
+          MotionEvent up = MotionEvent.obtain(SystemClock.uptimeMillis(),
+              SystemClock.uptimeMillis(),
+              MotionEvent.ACTION_UP,
+              xy[0],
+              xy[1],
+              0);
+
+          try {
+            injectEventWorked.set(injector.injectMotionEvent(up));
+          } catch (InjectEventSecurityException e) {
+            Log.e(TAG, "injectEvent threw a SecurityException");
+          }
+          up.recycle();
+          latch.countDown();
+        }
+      });
+
+      latch.await(10, TimeUnit.SECONDS);
+      assertFalse(injectEventWorked.get());
+    } finally {
+      ActivityLifecycleMonitorRegistry
+          .getInstance()
+          .removeLifecycleCallback(callback);
+    }
+
+  }
+}
diff --git a/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/base/IdlingResourceIntegrationTest.java b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/base/IdlingResourceIntegrationTest.java
new file mode 100644
index 0000000..98633f7
--- /dev/null
+++ b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/base/IdlingResourceIntegrationTest.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.base;
+
+import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView;
+import static com.google.android.apps.common.testing.ui.espresso.Espresso.pressBack;
+import static com.google.android.apps.common.testing.ui.espresso.Espresso.registerIdlingResources;
+import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click;
+import static com.google.android.apps.common.testing.ui.espresso.assertion.ViewAssertions.matches;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isDisplayed;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withText;
+import static org.hamcrest.Matchers.equalToIgnoringCase;
+import static org.hamcrest.Matchers.is;
+
+import com.google.android.apps.common.testing.ui.espresso.IdlingResource;
+import com.google.android.apps.common.testing.ui.testapp.SendActivity;
+
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Integration test with IdlingResources.
+ */
+@LargeTest
+public class IdlingResourceIntegrationTest extends ActivityInstrumentationTestCase2<SendActivity> {
+
+  private ResettingIdlingResource r1;
+  private ResettingIdlingResource r2;
+
+  @SuppressWarnings("deprecation")
+  public IdlingResourceIntegrationTest() {
+    super("com.google.android.apps.common.testing.ui.testapp", SendActivity.class);
+  }
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    r1 = new ResettingIdlingResource("SlowResource", 6000);
+    r2 = new ResettingIdlingResource("FastResource", 500);
+    registerIdlingResources(r1, r2);
+    getActivity();
+  }
+
+  public void testClickWithCustomIdlingResources() {
+    onView(withText(equalToIgnoringCase("send"))).perform(click());
+    r1.reset();
+    r2.reset();
+    onView(withText(is("Data from sender"))).check(matches(isDisplayed()));
+    r1.reset();
+    r2.reset();
+    pressBack();
+    r1.reset();
+    r2.reset();
+    onView(withText(equalToIgnoringCase("send"))).perform(click());
+    r1.reset();
+    r2.reset();
+    pressBack();
+    r1.reset();
+    r2.reset();
+    onView(withText(equalToIgnoringCase("send"))).perform(click());
+  }
+
+  private class ResettingIdlingResource implements IdlingResource {
+    private final String name;
+    private final long delay;
+    private final AtomicBoolean isIdle = new AtomicBoolean(false);
+    private final ScheduledExecutorService pool;
+    
+    private ResourceCallback callback;
+
+    public ResettingIdlingResource(String name, long delay) {
+      this.name = name;
+      this.delay = delay;
+      this.pool = Executors.newScheduledThreadPool(1);
+    }
+
+    @Override
+    public void registerIdleTransitionCallback(final ResourceCallback callback) {
+      this.callback = callback;
+      scheduleDelayedCallback();
+    }
+
+    private void scheduleDelayedCallback() {
+      pool.schedule(new Runnable() {
+        @Override
+        public void run() {
+          callback.onTransitionToIdle();
+          isIdle.set(true);
+        }
+      }, delay, TimeUnit.MILLISECONDS);
+    }
+
+    @Override
+    public boolean isIdleNow() {
+      return isIdle.get();
+    }
+
+    @Override
+    public String getName() {
+      return name;
+    }
+
+    public void reset() {
+      isIdle.set(false);
+      scheduleDelayedCallback();
+    }
+  }
+}
diff --git a/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/base/IdlingResourceRegistryTest.java b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/base/IdlingResourceRegistryTest.java
new file mode 100644
index 0000000..aee49a8
--- /dev/null
+++ b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/base/IdlingResourceRegistryTest.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.base;
+
+import com.google.android.apps.common.testing.ui.espresso.IdlingResource;
+import com.google.android.apps.common.testing.ui.espresso.base.IdlingResourceRegistry.IdleNotificationCallback;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Unit tests for {@link IdlingResourceRegistry}.
+ */
+public class IdlingResourceRegistryTest extends InstrumentationTestCase {
+
+  private IdlingResourceRegistry registry;
+  private Handler handler;
+
+  @Override
+  public void setUp() throws Exception {
+    Looper looper = Looper.getMainLooper();
+    handler = new Handler(looper);
+    registry = new IdlingResourceRegistry(looper);
+  }
+
+  public void testRegisterDuplicates() {
+    IdlingResource r1 = new OnDemandIdlingResource("r1");
+    IdlingResource r1dup = new OnDemandIdlingResource("r1");
+    registry.register(r1);
+    registry.register(r1);
+    registry.register(r1dup);
+  }
+
+  public void testAllResourcesAreIdle() throws InterruptedException {
+    OnDemandIdlingResource r1 = new OnDemandIdlingResource("r1");
+    OnDemandIdlingResource r2 = new OnDemandIdlingResource("r2");
+    IdlingResource r3 = new OnDemandIdlingResource("r3");
+    r1.forceIdleNow();
+    r2.forceIdleNow();
+    registry.register(r1);
+    registry.register(r2);
+    final AtomicBoolean resourcesIdle = new AtomicBoolean(false);
+    final CountDownLatch latch = new CountDownLatch(1);
+    handler.post(new Runnable() {
+      @Override
+      public void run() {
+        resourcesIdle.set(registry.allResourcesAreIdle());
+        latch.countDown();
+      }
+    });
+    latch.await();
+    assertTrue(resourcesIdle.get());
+
+    final CountDownLatch latch2 = new CountDownLatch(1);
+    registry.register(r3);
+    handler.post(new Runnable() {
+      @Override
+      public void run() {
+        resourcesIdle.set(registry.allResourcesAreIdle());
+        latch2.countDown();
+      }
+    });
+    latch2.await();
+    assertFalse(resourcesIdle.get());
+  }
+
+  @LargeTest
+  public void testAllResourcesAreIdle_RepeatingToIdleTransitions() throws InterruptedException {
+    OnDemandIdlingResource r1 = new OnDemandIdlingResource("r1");
+    registry.register(r1);
+    final AtomicBoolean resourcesIdle = new AtomicBoolean(false);
+    for (int i = 1; i <= 3; i++) {
+      final CountDownLatch latch = new CountDownLatch(1);
+      handler.post(new Runnable() {
+        @Override
+        public void run() {
+          resourcesIdle.set(registry.allResourcesAreIdle());
+          latch.countDown();
+        }
+      });
+      latch.await();
+      assertFalse("Busy test " + i, resourcesIdle.get());
+
+      r1.forceIdleNow();
+      final CountDownLatch latch2 = new CountDownLatch(1);
+      handler.post(new Runnable() {
+        @Override
+        public void run() {
+          resourcesIdle.set(registry.allResourcesAreIdle());
+          latch2.countDown();
+        }
+      });
+      latch2.await();
+      assertTrue("Idle transition test " + i, resourcesIdle.get());
+
+      r1.reset();
+    }
+  }
+
+  @LargeTest
+  public void testNotifyWhenAllResourcesAreIdle_success() throws InterruptedException {
+    final CountDownLatch busyWarningLatch = new CountDownLatch(4);
+    final CountDownLatch timeoutLatch = new CountDownLatch(1);
+    final CountDownLatch allResourcesIdleLatch = new CountDownLatch(1);
+    final AtomicReference<List<String>> busysFromWarning = new AtomicReference<List<String>>();
+
+    OnDemandIdlingResource r1 = new OnDemandIdlingResource("r1");
+    OnDemandIdlingResource r2 = new OnDemandIdlingResource("r2");
+    OnDemandIdlingResource r3 = new OnDemandIdlingResource("r3");
+    registry.register(r1);
+    registry.register(r2);
+    registry.register(r3);
+
+    handler.post(new Runnable() {
+
+      @Override
+      public void run() {
+        registry.notifyWhenAllResourcesAreIdle(new IdleNotificationCallback() {
+          private static final String TAG = "IdleNotificationCallback";
+          @Override
+          public void resourcesStillBusyWarning(List<String> busyResourceNames) {
+            Log.w(TAG, "Timeout warning: " + busyResourceNames);
+            busysFromWarning.set(busyResourceNames);
+            busyWarningLatch.countDown();
+          }
+
+          @Override
+          public void resourcesHaveTimedOut(List<String> busyResourceNames) {
+            Log.w(TAG, "Timeout error: " + busyResourceNames);
+            timeoutLatch.countDown();
+          }
+
+          @Override
+          public void allResourcesIdle() {
+            allResourcesIdleLatch.countDown();
+          }
+        });
+      }
+    });
+
+    assertFalse("Expected to timeout", busyWarningLatch.await(6, TimeUnit.SECONDS));
+    assertEquals(3, busysFromWarning.get().size());
+
+    r3.forceIdleNow();
+    assertFalse("Expected to timeout", busyWarningLatch.await(6, TimeUnit.SECONDS));
+    assertEquals(2, busysFromWarning.get().size());
+
+    r2.forceIdleNow();
+    assertFalse("Expected to timeout", busyWarningLatch.await(6, TimeUnit.SECONDS));
+    assertEquals(1, busysFromWarning.get().size());
+
+    r1.forceIdleNow();
+    assertTrue(allResourcesIdleLatch.await(200, TimeUnit.MILLISECONDS));
+    assertEquals(1, busyWarningLatch.getCount());
+    assertEquals(1, timeoutLatch.getCount());
+  }
+
+  @LargeTest
+  public void testNotifyWhenAllResourcesAreIdle_timeout() throws InterruptedException {
+    final CountDownLatch busyWarningLatch = new CountDownLatch(5);
+    final CountDownLatch timeoutLatch = new CountDownLatch(1);
+    final CountDownLatch allResourcesIdleLatch = new CountDownLatch(1);
+    final AtomicReference<List<String>> busysFromWarning = new AtomicReference<List<String>>();
+
+    OnDemandIdlingResource r1 = new OnDemandIdlingResource("r1");
+    OnDemandIdlingResource r2 = new OnDemandIdlingResource("r2");
+    OnDemandIdlingResource r3 = new OnDemandIdlingResource("r3");
+    registry.register(r1);
+    registry.register(r2);
+    registry.register(r3);
+
+    handler.post(new Runnable() {
+      @Override
+      public void run() {
+        registry.notifyWhenAllResourcesAreIdle(new IdleNotificationCallback() {
+          private static final String TAG = "IdleNotificationCallback";
+          @Override
+          public void resourcesStillBusyWarning(List<String> busyResourceNames) {
+            Log.w(TAG, "Timeout warning: " + busyResourceNames);
+            busysFromWarning.set(busyResourceNames);
+            busyWarningLatch.countDown();
+          }
+
+          @Override
+          public void resourcesHaveTimedOut(List<String> busyResourceNames) {
+            Log.w(TAG, "Timeout error: " + busyResourceNames);
+            timeoutLatch.countDown();
+          }
+
+          @Override
+          public void allResourcesIdle() {
+            allResourcesIdleLatch.countDown();
+          }
+        });
+      }
+    });
+
+    assertFalse("Expected to timeout", busyWarningLatch.await(6, TimeUnit.SECONDS));
+    assertEquals(3, busysFromWarning.get().size());
+
+    r1.forceIdleNow();
+    assertFalse("Expected to timeout", busyWarningLatch.await(6, TimeUnit.SECONDS));
+    assertEquals(2, busysFromWarning.get().size());
+
+    r2.forceIdleNow();
+    assertFalse("Expected to timeout", busyWarningLatch.await(6, TimeUnit.SECONDS));
+    assertEquals(1, busysFromWarning.get().size());
+
+    assertTrue("Expected to finish count down", busyWarningLatch.await(8, TimeUnit.SECONDS));
+    assertTrue("Should have timed out", timeoutLatch.await(2, TimeUnit.SECONDS));
+    assertEquals(1, busysFromWarning.get().size());
+    assertEquals(1, allResourcesIdleLatch.getCount());
+  }
+}
diff --git a/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/base/OnDemandIdlingResource.java b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/base/OnDemandIdlingResource.java
new file mode 100644
index 0000000..ac0a5a7
--- /dev/null
+++ b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/base/OnDemandIdlingResource.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.base;
+
+import com.google.android.apps.common.testing.ui.espresso.IdlingResource;
+
+/**
+ * An {@link IdlingResource} for testing that becomes idle on demand.
+ */
+public class OnDemandIdlingResource implements IdlingResource {
+  private final String name;
+
+  private boolean isIdle = false;
+  private ResourceCallback callback;
+
+  public OnDemandIdlingResource(String name) {
+    this.name = name;
+  }
+
+  @Override
+  public void registerIdleTransitionCallback(ResourceCallback callback) {
+    this.callback = callback;
+  }
+
+  @Override
+  public boolean isIdleNow() {
+    return isIdle;
+  }
+
+  @Override
+  public String getName() {
+    return name;
+  }
+
+  public void forceIdleNow() {
+    isIdle = true;
+    if (callback != null) {
+      callback.onTransitionToIdle();
+    }
+  }
+
+  public void reset() {
+    isIdle = false;
+  }
+}
diff --git a/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/base/UiControllerImplIntegrationTest.java b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/base/UiControllerImplIntegrationTest.java
new file mode 100644
index 0000000..02a3fda
--- /dev/null
+++ b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/base/UiControllerImplIntegrationTest.java
@@ -0,0 +1,295 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.base;
+
+import com.google.android.apps.common.testing.ui.espresso.InjectEventSecurityException;
+import com.google.android.apps.common.testing.ui.espresso.UiController;
+import com.google.android.apps.common.testing.ui.espresso.util.HumanReadables;
+import com.google.android.apps.common.testing.ui.testapp.R;
+import com.google.android.apps.common.testing.ui.testapp.SendActivity;
+import com.google.common.base.Optional;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.os.Build;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Test for {@link UiControllerImpl}.
+ */
+public class UiControllerImplIntegrationTest
+    extends ActivityInstrumentationTestCase2<SendActivity> {
+  private Activity sendActivity;
+  private final AtomicBoolean injectEventWorked = new AtomicBoolean(false);
+  private final AtomicBoolean injectEventThrewSecurityException = new AtomicBoolean(false);
+  private final CountDownLatch focusLatch = new CountDownLatch(1);
+  private final CountDownLatch latch = new CountDownLatch(1);
+  private UiController uiController;
+
+  @SuppressWarnings("deprecation")
+  public UiControllerImplIntegrationTest() {
+    // Supporting froyo.
+    super("com.google.android.apps.common.testing.ui.testapp", SendActivity.class);
+  }
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    EventInjector injector = null;
+    if (Build.VERSION.SDK_INT > 15) {
+      InputManagerEventInjectionStrategy strat = new InputManagerEventInjectionStrategy();
+      strat.initialize();
+      injector = new EventInjector(strat);
+    } else {
+      WindowManagerEventInjectionStrategy strat = new WindowManagerEventInjectionStrategy();
+      strat.initialize();
+      injector = new EventInjector(strat);
+    }
+    uiController = new UiControllerImpl(
+        injector,
+        new AsyncTaskPoolMonitor(new ThreadPoolExecutorExtractor(
+            Looper.getMainLooper()).getAsyncTaskThreadPool()),
+        Optional.<AsyncTaskPoolMonitor>absent(),
+        new IdlingResourceRegistry(Looper.getMainLooper()),
+        Looper.getMainLooper());
+  }
+
+
+  @Override
+  public SendActivity getActivity() {
+    SendActivity a = super.getActivity();
+
+    while (!a.hasWindowFocus()) {
+      getInstrumentation().waitForIdleSync();
+    }
+
+    return a;
+  }
+
+  @LargeTest
+  public void testInjectKeyEvent() throws InterruptedException {
+    sendActivity = getActivity();
+    getInstrumentation().waitForIdleSync();
+
+    getInstrumentation().runOnMainSync(new Runnable() {
+      @Override
+      public void run() {
+        try {
+          KeyCharacterMap keyCharacterMap = UiControllerImpl.getKeyCharacterMap();
+          KeyEvent[] events = keyCharacterMap.getEvents("a".toCharArray());
+          injectEventWorked.set(uiController.injectKeyEvent(events[0]));
+          latch.countDown();
+        } catch (InjectEventSecurityException e) {
+          injectEventThrewSecurityException.set(true);
+        }
+      }
+    });
+
+    assertFalse("injectEvent threw a SecurityException", injectEventThrewSecurityException.get());
+    assertTrue("Timed out!", latch.await(10, TimeUnit.SECONDS));
+    assertTrue(injectEventWorked.get());
+  }
+
+  @LargeTest
+  public void testInjectString() throws InterruptedException {
+    sendActivity = getActivity();
+    getInstrumentation().waitForIdleSync();
+    final AtomicBoolean requestFocusSucceded = new AtomicBoolean(false);
+
+    getInstrumentation().runOnMainSync(new Runnable() {
+      @Override
+      public void run() {
+        final View view = sendActivity.findViewById(R.id.send_data_to_call_edit_text);
+        Log.i("TEST", HumanReadables.describe(view));
+        requestFocusSucceded.set(view.requestFocus() && view.hasWindowFocus());
+        Log.i("TEST-post", HumanReadables.describe(view));
+        focusLatch.countDown();
+      }
+    });
+
+    getInstrumentation().waitForIdleSync();
+    assertTrue("requestFocus timed out!", focusLatch.await(2, TimeUnit.SECONDS));
+    assertTrue("requestFocus failed.", requestFocusSucceded.get());
+
+    getInstrumentation().runOnMainSync(new Runnable() {
+      @Override
+      public void run() {
+        try {
+          injectEventWorked.set(uiController.injectString("Hello! \n&*$$$"));
+          latch.countDown();
+        } catch (InjectEventSecurityException e) {
+          injectEventThrewSecurityException.set(true);
+        }
+      }
+    });
+
+    assertFalse("SecurityException exception was thrown.", injectEventThrewSecurityException.get());
+    assertTrue("Timed out!", latch.await(20, TimeUnit.SECONDS));
+    assertTrue(injectEventWorked.get());
+  }
+
+  @LargeTest
+  public void testInjectLargeString() throws InterruptedException {
+    sendActivity = getActivity();
+    getInstrumentation().waitForIdleSync();
+    final AtomicBoolean requestFocusSucceded = new AtomicBoolean(false);
+
+    getInstrumentation().runOnMainSync(new Runnable() {
+      @Override
+      public void run() {
+        final View view = sendActivity.findViewById(R.id.send_data_to_call_edit_text);
+        Log.i("TEST", HumanReadables.describe(view));
+        requestFocusSucceded.set(view.requestFocus());
+        Log.i("TEST-post", HumanReadables.describe(view));
+
+        focusLatch.countDown();
+      }
+    });
+
+    assertTrue("requestFocus timed out!", focusLatch.await(2, TimeUnit.SECONDS));
+    assertTrue("requestFocus failed.", requestFocusSucceded.get());
+
+    getInstrumentation().runOnMainSync(new Runnable() {
+      @Override
+      public void run() {
+        try {
+          injectEventWorked.set(uiController.injectString("This is a string with 32 chars!!"));
+          latch.countDown();
+        } catch (InjectEventSecurityException e) {
+          injectEventThrewSecurityException.set(true);
+        }
+      }
+    });
+
+    assertFalse("SecurityException exception was thrown.", injectEventThrewSecurityException.get());
+    assertTrue("Timed out!", latch.await(20, TimeUnit.SECONDS));
+    assertTrue(injectEventWorked.get());
+  }
+
+  @LargeTest
+  public void testInjectEmptyString() throws InterruptedException {
+    sendActivity = getActivity();
+    getInstrumentation().waitForIdleSync();
+    final AtomicBoolean requestFocusSucceded = new AtomicBoolean(false);
+
+    getInstrumentation().runOnMainSync(new Runnable() {
+      @Override
+      public void run() {
+        final View view = sendActivity.findViewById(R.id.send_data_to_call_edit_text);
+        requestFocusSucceded.set(view.requestFocus());
+        focusLatch.countDown();
+      }
+    });
+
+    assertTrue("requestFocus timed out!", focusLatch.await(2, TimeUnit.SECONDS));
+    assertTrue("requestFocus failed.", requestFocusSucceded.get());
+
+    getInstrumentation().runOnMainSync(new Runnable() {
+      @Override
+      public void run() {
+        try {
+          injectEventWorked.set(uiController.injectString(""));
+          latch.countDown();
+        } catch (InjectEventSecurityException e) {
+          injectEventThrewSecurityException.set(true);
+        }
+      }
+    });
+
+    assertFalse("SecurityException exception was thrown.", injectEventThrewSecurityException.get());
+    assertTrue("Timed out!", latch.await(20, TimeUnit.SECONDS));
+    assertTrue(injectEventWorked.get());
+  }
+
+  @LargeTest
+  public void testInjectStringSecurityException() throws InterruptedException {
+    getInstrumentation().runOnMainSync(new Runnable() {
+      @Override
+      public void run() {
+        try {
+          injectEventWorked.set(uiController.injectString("Hello! \n&*$$$"));
+          latch.countDown();
+        } catch (InjectEventSecurityException e) {
+          injectEventThrewSecurityException.set(true);
+        }
+      }
+    });
+
+    assertTrue("SecurityException exception was thrown.", injectEventThrewSecurityException.get());
+    assertFalse("Did NOT time out!", latch.await(3, TimeUnit.SECONDS));
+    assertFalse(injectEventWorked.get());
+  }
+
+  @LargeTest
+  public void testInjectMotionEvent() throws InterruptedException {
+    sendActivity = getActivity();
+    final int xy[] = getCoordinatesInMiddleOfSendButton(sendActivity, getInstrumentation());
+
+    getInstrumentation().runOnMainSync(new Runnable() {
+      @Override
+      public void run() {
+        long downTime = SystemClock.uptimeMillis();
+        try {
+          MotionEvent event = MotionEvent.obtain(downTime,
+              SystemClock.uptimeMillis(),
+              MotionEvent.ACTION_DOWN,
+              xy[0],
+              xy[1],
+              0);
+
+          injectEventWorked.set(uiController.injectMotionEvent(event));
+          event.recycle();
+          latch.countDown();
+        } catch (InjectEventSecurityException e) {
+          injectEventThrewSecurityException.set(true);
+        }
+      }
+    });
+
+    assertFalse("SecurityException exception was thrown.", injectEventThrewSecurityException.get());
+    assertTrue("Timed out!", latch.await(10, TimeUnit.SECONDS));
+    assertTrue(injectEventWorked.get());
+  }
+
+  static int[] getCoordinatesInMiddleOfSendButton(
+      Activity activity, Instrumentation instrumentation) {
+    final View sendButton = activity.findViewById(R.id.send_button);
+    final int[] xy = new int[2];
+    instrumentation.runOnMainSync(new Runnable() {
+      @Override
+      public void run() {
+        sendButton.getLocationOnScreen(xy);
+      }
+    });
+    int x = xy[0] + (sendButton.getWidth() / 2);
+    int y = xy[1] + (sendButton.getHeight() / 2);
+    int[] xyMiddle = {x, y};
+    return xyMiddle;
+  }
+}
diff --git a/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/base/UiControllerImplTest.java b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/base/UiControllerImplTest.java
new file mode 100644
index 0000000..2b95fc8
--- /dev/null
+++ b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/base/UiControllerImplTest.java
@@ -0,0 +1,390 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.base;
+
+import com.google.android.apps.common.testing.ui.espresso.IdlingResourceTimeoutException;
+import com.google.common.base.Optional;
+
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+
+import junit.framework.TestCase;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Unit test for {@link UiControllerImpl}.
+ */
+public class UiControllerImplTest extends TestCase {
+
+  private static final String TAG = UiControllerImplTest.class.getSimpleName();
+
+  private LooperThread testThread;
+  private AtomicReference<UiControllerImpl> uiController = new AtomicReference<UiControllerImpl>();
+  private ThreadPoolExecutor asyncPool;
+  private IdlingResourceRegistry idlingResourceRegistry;
+
+  private static class LooperThread extends Thread {
+    private final CountDownLatch init = new CountDownLatch(1);
+    private Handler handler;
+    private Looper looper;
+
+    @Override
+    public void run() {
+      Looper.prepare();
+      handler = new Handler();
+      looper = Looper.myLooper();
+      init.countDown();
+      Looper.loop();
+    }
+
+    public void quitLooper() {
+      looper.quit();
+    }
+
+    public Looper getLooper() {
+      try {
+        init.await();
+      } catch (InterruptedException ie) {
+        Thread.currentThread().interrupt();
+      }
+      return looper;
+    }
+
+    public Handler getHandler() {
+      try {
+        init.await();
+      } catch (InterruptedException ie) {
+        Thread.currentThread().interrupt();
+      }
+      return handler;
+    }
+  }
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    testThread = new LooperThread();
+    testThread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
+      @Override
+      public void uncaughtException(Thread thread, Throwable ex) {
+        Log.e(TAG, "Looper died: ", ex);
+      }
+    });
+    testThread.start();
+    idlingResourceRegistry = new IdlingResourceRegistry(testThread.getLooper());
+    asyncPool = new ThreadPoolExecutor(3, 3, 1, TimeUnit.SECONDS,
+        new LinkedBlockingQueue<Runnable>());
+    EventInjector injector = null;
+    if (Build.VERSION.SDK_INT > 15) {
+      InputManagerEventInjectionStrategy strat = new InputManagerEventInjectionStrategy();
+      strat.initialize();
+      injector = new EventInjector(strat);
+    } else {
+      WindowManagerEventInjectionStrategy strat = new WindowManagerEventInjectionStrategy();
+      strat.initialize();
+      injector = new EventInjector(strat);
+    }
+    uiController.set(new UiControllerImpl(
+        injector,
+        new AsyncTaskPoolMonitor(asyncPool),
+        Optional.<AsyncTaskPoolMonitor>absent(),
+        idlingResourceRegistry,
+        testThread.getLooper()
+        ));
+
+
+  }
+
+  @Override
+  public void tearDown() throws Exception {
+    testThread.quitLooper();
+    asyncPool.shutdown();
+    super.tearDown();
+  }
+
+  public void testLoopMainThreadTillIdle_sendsMessageToRightHandler() {
+    final CountDownLatch latch = new CountDownLatch(3);
+    testThread.getHandler(); // blocks till initialized;
+    final Handler firstHandler = new Handler(
+        testThread.looper,
+        new Handler.Callback() {
+          private boolean counted = false;
+          @Override
+          public boolean handleMessage(Message me) {
+            if (counted) {
+              fail("Called 2x!!!!");
+            }
+            counted = true;
+            latch.countDown();
+            return true;
+          }
+        });
+
+    final Handler secondHandler = new Handler(
+        testThread.looper,
+        new Handler.Callback() {
+          private boolean counted = false;
+          @Override
+          public boolean handleMessage(Message me) {
+            if (counted) {
+              fail("Called 2x!!!!");
+            }
+            counted = true;
+            latch.countDown();
+            return true;
+          }
+        });
+
+    assertTrue(testThread.getHandler().post(new Runnable() {
+      @Override
+      public void run() {
+        firstHandler.sendEmptyMessage(1);
+        secondHandler.sendEmptyMessage(1);
+        uiController.get().loopMainThreadUntilIdle();
+
+        latch.countDown();
+      }
+    }));
+
+    try {
+      assertTrue(
+          "Timed out waiting for looper to process all events", latch.await(10, TimeUnit.SECONDS));
+    } catch (InterruptedException e) {
+      fail("Failed with exception " + e);
+    }
+  }
+
+  public void testLoopForAtLeast() throws Exception {
+    final CountDownLatch latch = new CountDownLatch(2);
+    assertTrue(testThread.getHandler().post(new Runnable() {
+      @Override
+      public void run() {
+        testThread.getHandler().post(new Runnable() {
+          @Override
+          public void run() {
+            latch.countDown();
+          }
+
+        });
+        uiController.get().loopMainThreadForAtLeast(1000);
+        latch.countDown();
+      }
+    }));
+    assertTrue("Never returned from UiControllerImpl.loopMainThreadForAtLeast();",
+        latch.await(10, TimeUnit.SECONDS));
+  }
+
+  public void testLoopMainThreadUntilIdle_fullQueue() {
+    final CountDownLatch latch = new CountDownLatch(3);
+    assertTrue(testThread.getHandler().post(new Runnable() {
+      @Override
+      public void run() {
+        Log.i(TAG, "On main thread");
+        Handler handler = new Handler();
+        Log.i(TAG, "Equeueing test runnable 1");
+        handler.post(new Runnable() {
+          @Override
+          public void run() {
+            Log.i(TAG, "Running test runnable 1");
+            latch.countDown();
+          }
+        });
+        Log.i(TAG, "Equeueing test runnable 2");
+        handler.post(new Runnable() {
+          @Override
+          public void run() {
+            Log.i(TAG, "Running test runnable 2");
+            latch.countDown();
+          }
+        });
+        Log.i(TAG, "Hijacking thread and looping it.");
+        uiController.get().loopMainThreadUntilIdle();
+        latch.countDown();
+      }
+    }));
+
+    try {
+      assertTrue(
+          "Timed out waiting for looper to process all events", latch.await(10, TimeUnit.SECONDS));
+    } catch (InterruptedException e) {
+      fail("Failed with exception " + e);
+    }
+  }
+
+  public void testLoopMainThreadUntilIdle_fullQueueAndAsyncTasks() throws Exception {
+    final CountDownLatch latch = new CountDownLatch(3);
+    final CountDownLatch asyncTaskStarted = new CountDownLatch(1);
+    final CountDownLatch asyncTaskShouldComplete = new CountDownLatch(1);
+    asyncPool.execute(new Runnable() {
+      @Override
+      public void run() {
+        asyncTaskStarted.countDown();
+        while (true) {
+          try {
+            asyncTaskShouldComplete.await();
+            return;
+          } catch (InterruptedException ie) {
+            // cant interrupt me. ignore.
+          }
+        }
+      }
+    });
+    assertTrue("async task is not starting!", asyncTaskStarted.await(2, TimeUnit.SECONDS));
+
+    assertTrue(testThread.getHandler().post(new Runnable() {
+      @Override
+      public void run() {
+        Log.i(TAG, "On main thread");
+        Handler handler = new Handler();
+        Log.i(TAG, "Equeueing test runnable 1");
+        handler.post(new Runnable() {
+          @Override
+          public void run() {
+            Log.i(TAG, "Running test runnable 1");
+            latch.countDown();
+          }
+        });
+        Log.i(TAG, "Equeueing test runnable 2");
+        handler.post(new Runnable() {
+          @Override
+          public void run() {
+            Log.i(TAG, "Running test runnable 2");
+            latch.countDown();
+          }
+        });
+        Log.i(TAG, "Hijacking thread and looping it.");
+        uiController.get().loopMainThreadUntilIdle();
+        latch.countDown();
+      }
+    }));
+    assertFalse(
+        "Should not have stopped looping the main thread yet!", latch.await(2, TimeUnit.SECONDS));
+    assertEquals("Not all main thread tasks have checked in", 1L, latch.getCount());
+    asyncTaskShouldComplete.countDown();
+    assertTrue("App should be idle.", latch.await(5, TimeUnit.SECONDS));
+  }
+
+
+  public void testLoopMainThreadUntilIdle_emptyQueue() {
+    final CountDownLatch latch = new CountDownLatch(1);
+    assertTrue(testThread.getHandler().post(new Runnable() {
+      @Override
+      public void run() {
+        uiController.get().loopMainThreadUntilIdle();
+        latch.countDown();
+      }
+    }));
+    try {
+      assertTrue("Never returned from UiControllerImpl.loopMainThreadUntilIdle();",
+          latch.await(10, TimeUnit.SECONDS));
+    } catch (InterruptedException e) {
+      fail("Failed with exception " + e);
+    }
+  }
+
+  public void testLoopMainThreadUntilIdle_oneIdlingResource() throws InterruptedException {
+    OnDemandIdlingResource fakeResource = new OnDemandIdlingResource("FakeResource");
+    idlingResourceRegistry.register(fakeResource);
+    final CountDownLatch latch = new CountDownLatch(1);
+    assertTrue(testThread.getHandler().post(new Runnable() {
+      @Override
+      public void run() {
+        Log.i(TAG, "Hijacking thread and looping it.");
+        uiController.get().loopMainThreadUntilIdle();
+        latch.countDown();
+      }
+    }));
+    assertFalse(
+        "Should not have stopped looping the main thread yet!", latch.await(2, TimeUnit.SECONDS));
+    fakeResource.forceIdleNow();
+    assertTrue("App should be idle.", latch.await(5, TimeUnit.SECONDS));
+  }
+
+  public void testLoopMainThreadUntilIdle_multipleIdlingResources() throws InterruptedException {
+    OnDemandIdlingResource fakeResource1 = new OnDemandIdlingResource("FakeResource1");
+    OnDemandIdlingResource fakeResource2 = new OnDemandIdlingResource("FakeResource2");
+    OnDemandIdlingResource fakeResource3 = new OnDemandIdlingResource("FakeResource3");
+    // Register the first two right away and one later (once the wait for the first two begins).
+    idlingResourceRegistry.register(fakeResource1);
+    idlingResourceRegistry.register(fakeResource2);
+    final CountDownLatch latch = new CountDownLatch(1);
+    assertTrue(testThread.getHandler().post(new Runnable() {
+      @Override
+      public void run() {
+        Log.i(TAG, "Hijacking thread and looping it.");
+        uiController.get().loopMainThreadUntilIdle();
+        latch.countDown();
+      }
+    }));
+    assertFalse(
+        "Should not have stopped looping the main thread yet!", latch.await(1, TimeUnit.SECONDS));
+    fakeResource1.forceIdleNow();
+    assertFalse(
+        "Should not have stopped looping the main thread yet!", latch.await(1, TimeUnit.SECONDS));
+    idlingResourceRegistry.register(fakeResource3);
+    assertFalse(
+        "Should not have stopped looping the main thread yet!", latch.await(1, TimeUnit.SECONDS));
+    fakeResource2.forceIdleNow();
+    assertFalse(
+        "Should not have stopped looping the main thread yet!", latch.await(1, TimeUnit.SECONDS));
+    fakeResource3.forceIdleNow();
+    assertTrue("App should be idle.", latch.await(5, TimeUnit.SECONDS));
+  }
+
+  @LargeTest
+  public void testLoopMainThreadUntilIdle_timeout() throws InterruptedException {
+    OnDemandIdlingResource goodResource =
+        new OnDemandIdlingResource("GoodResource");
+    OnDemandIdlingResource kindaCrappyResource =
+        new OnDemandIdlingResource("KindaCrappyResource");
+    OnDemandIdlingResource badResource =
+        new OnDemandIdlingResource("VeryBadResource");
+    idlingResourceRegistry.register(goodResource);
+    idlingResourceRegistry.register(kindaCrappyResource);
+    idlingResourceRegistry.register(badResource);
+    final CountDownLatch latch = new CountDownLatch(1);
+    assertTrue(testThread.getHandler().post(new Runnable() {
+      @Override
+      public void run() {
+        Log.i(TAG, "Hijacking thread and looping it.");
+        try {
+          uiController.get().loopMainThreadUntilIdle();
+        } catch (IdlingResourceTimeoutException e) {
+          latch.countDown();
+        }
+      }
+    }));
+    assertFalse(
+        "Should not have stopped looping the main thread yet!", latch.await(4, TimeUnit.SECONDS));
+    goodResource.forceIdleNow();
+    assertFalse(
+        "Should not have stopped looping the main thread yet!", latch.await(12, TimeUnit.SECONDS));
+    kindaCrappyResource.forceIdleNow();
+    assertTrue(
+        "Should have caught IdlingResourceTimeoutException", latch.await(11, TimeUnit.SECONDS));
+  }
+
+}
diff --git a/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/base/ViewFinderImplTest.java b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/base/ViewFinderImplTest.java
new file mode 100644
index 0000000..7810a83
--- /dev/null
+++ b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/base/ViewFinderImplTest.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.base;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.sameInstance;
+
+import com.google.android.apps.common.testing.ui.espresso.AmbiguousViewMatcherException;
+import com.google.android.apps.common.testing.ui.espresso.NoMatchingViewException;
+import com.google.android.apps.common.testing.ui.espresso.ViewFinder;
+
+import android.test.InstrumentationTestCase;
+import android.test.UiThreadTest;
+import android.view.View;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import org.hamcrest.Matchers;
+
+import javax.inject.Provider;
+
+/** Unit tests for {@link ViewFinderImpl}. */
+public class ViewFinderImplTest extends InstrumentationTestCase {
+  private Provider<View> testViewProvider;
+  private RelativeLayout testView;
+  private View child1;
+  private View child2;
+  private View child3;
+  private View child4;
+  private View nestedChild;
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    testView = new RelativeLayout(getInstrumentation().getTargetContext());
+    child1 = new TextView(getInstrumentation().getTargetContext());
+    child1.setId(1);
+    child2 = new TextView(getInstrumentation().getTargetContext());
+    child2.setId(2);
+    child3 = new TextView(getInstrumentation().getTargetContext());
+    child3.setId(3);
+    child4 = new TextView(getInstrumentation().getTargetContext());
+    child4.setId(4);
+    nestedChild = new TextView(getInstrumentation().getTargetContext());
+    nestedChild.setId(5);
+    RelativeLayout nestingLayout = new RelativeLayout(getInstrumentation().getTargetContext());
+    nestingLayout.addView(nestedChild);
+    testView.addView(child1);
+    testView.addView(child2);
+    testView.addView(nestingLayout);
+    testView.addView(child3);
+    testView.addView(child4);
+    testViewProvider = new Provider<View>() {
+      @Override
+      public View get() {
+        return testView;
+      }
+
+      @Override
+      public String toString() {
+        return "of(" + testView + ")";
+      }
+    };
+  }
+
+  @UiThreadTest
+  public void testGetView_present() {
+    ViewFinder finder = new ViewFinderImpl(sameInstance(nestedChild), testViewProvider);
+    assertThat(finder.getView(), sameInstance(nestedChild));
+  }
+
+  @UiThreadTest
+  public void testGetView_missing() {
+    ViewFinder finder = new ViewFinderImpl(Matchers.<View>nullValue(), testViewProvider);
+    try {
+      finder.getView();
+      fail("No children should pass that matcher!");
+    } catch (NoMatchingViewException expected) {}
+  }
+
+  @UiThreadTest
+  public void testGetView_multiple() {
+    ViewFinder finder = new ViewFinderImpl(Matchers.<View>notNullValue(), testViewProvider);
+    try {
+      finder.getView();
+      fail("All nodes hit that matcher!");
+    } catch (AmbiguousViewMatcherException expected) {}
+  }
+
+  public void testFind_offUiThread() {
+    ViewFinder finder = new ViewFinderImpl(sameInstance(nestedChild), testViewProvider);
+    try {
+      finder.getView();
+      fail("not on main thread, should die.");
+    } catch (IllegalStateException expected) {}
+  }
+
+}
diff --git a/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/contrib/CountingIdlingResourceTest.java b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/contrib/CountingIdlingResourceTest.java
new file mode 100644
index 0000000..8bd2d11
--- /dev/null
+++ b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/contrib/CountingIdlingResourceTest.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.contrib;
+
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import com.google.android.apps.common.testing.ui.espresso.IdlingResource.ResourceCallback;
+
+import android.test.InstrumentationTestCase;
+
+import org.mockito.Mock;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+
+/** Unit tests for {@link CountingIdlingResource}. */
+public class CountingIdlingResourceTest extends InstrumentationTestCase {
+
+  private static final String RESOURCE_NAME = "test_resource";
+  private CountingIdlingResource resource;
+
+  @Mock
+  private ResourceCallback mockCallback;
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    initMocks(this);
+    resource = new CountingIdlingResource(RESOURCE_NAME, true);
+  }
+
+  public void testResourceName() {
+    assertEquals(RESOURCE_NAME, resource.getName());
+  }
+
+  public void testInvalidStateDetected() throws Exception {
+    resource.increment();
+    resource.decrement();
+    try {
+      resource.decrement();
+      fail("Should throw illegal state exception!");
+    } catch (IllegalStateException expected) { }
+  }
+
+  public void testIsIdle() throws Exception {
+    assertTrue(callIsIdle());
+    resource.increment();
+    assertFalse(callIsIdle());
+    resource.decrement();
+    assertTrue(callIsIdle());
+  }
+
+  public void testIdleNotification() throws Exception {
+    registerIdleCallback();
+    assertTrue(callIsIdle());
+    verify(mockCallback, never()).onTransitionToIdle();
+
+    resource.increment();
+    verify(mockCallback, never()).onTransitionToIdle();
+    assertFalse(callIsIdle());
+
+    resource.decrement();
+    verify(mockCallback).onTransitionToIdle();
+    assertTrue(callIsIdle());
+  }
+
+  private void registerIdleCallback() throws Exception {
+    FutureTask<Void> registerTask = new FutureTask<Void>(new Callable<Void>() {
+      @Override
+      public Void call() throws Exception {
+        resource.registerIdleTransitionCallback(mockCallback);
+        return null;
+      }
+
+    });
+    getInstrumentation().runOnMainSync(registerTask);
+    try {
+      registerTask.get();
+    } catch (ExecutionException ee) {
+      throw new RuntimeException(ee.getCause());
+    }
+
+  }
+
+  private boolean callIsIdle() throws Exception {
+    FutureTask<Boolean> isIdleTask = new FutureTask<Boolean>(new IsIdleCallable());
+    getInstrumentation().runOnMainSync(isIdleTask);
+    try {
+      return isIdleTask.get();
+    } catch (ExecutionException ee) {
+      throw new RuntimeException(ee.getCause());
+    }
+  }
+
+
+  private class IsIdleCallable implements Callable<Boolean> {
+    @Override
+    public Boolean call() throws Exception {
+      return resource.isIdleNow();
+    }
+  }
+
+}
diff --git a/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/matcher/PreferenceMatchersTest.java b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/matcher/PreferenceMatchersTest.java
new file mode 100644
index 0000000..1501184
--- /dev/null
+++ b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/matcher/PreferenceMatchersTest.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.matcher;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+
+import static com.google.android.apps.common.testing.ui.espresso.matcher.PreferenceMatchers.withKey;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.PreferenceMatchers.withSummary;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.PreferenceMatchers.withSummaryText;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.PreferenceMatchers.withTitle;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.PreferenceMatchers.withTitleText;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.PreferenceMatchers.isEnabled;
+import static org.hamcrest.Matchers.not;
+
+import com.google.android.apps.common.testing.ui.testapp.test.R;
+
+import android.test.InstrumentationTestCase;
+import android.preference.CheckBoxPreference;
+import android.preference.EditTextPreference;
+
+/**
+ * Unit tests for preference matchers.
+ */
+public class PreferenceMatchersTest extends InstrumentationTestCase {
+
+
+  public void testWithSummary() {
+    CheckBoxPreference pref = new CheckBoxPreference(getInstrumentation().getContext());
+    pref.setSummary(R.string.something);
+    assertThat(pref, withSummary(R.string.something));
+    assertThat(pref, not(withSummary(R.string.other_string)));
+    assertThat(pref, withSummaryText("Hello World"));
+    assertThat(pref, not(withSummaryText(("Hello Mars"))));
+    assertThat(pref, withSummaryText(is("Hello World")));
+  }
+
+  public void testWithTitle() {
+    CheckBoxPreference pref = new CheckBoxPreference(getInstrumentation().getContext());
+    pref.setTitle(R.string.other_string);
+    assertThat(pref, withTitle(R.string.other_string));
+    assertThat(pref, not(withTitle(R.string.something)));
+    assertThat(pref, withTitleText("Goodbye!!"));
+    assertThat(pref, not(withTitleText(("Hello Mars"))));
+    assertThat(pref, withTitleText(is("Goodbye!!")));
+  }
+
+
+  public void testIsEnabled() {
+    CheckBoxPreference pref = new CheckBoxPreference(getInstrumentation().getContext());
+    pref.setEnabled(true);
+    assertThat(pref, isEnabled());
+    pref.setEnabled(false);
+    assertThat(pref, not(isEnabled()));
+    EditTextPreference pref2 = new EditTextPreference(getInstrumentation().getContext());
+    pref2.setEnabled(true);
+    assertThat(pref2, isEnabled());
+    pref2.setEnabled(false);
+    assertThat(pref2, not(isEnabled()));
+  }
+
+  public void testWithKey() {
+    CheckBoxPreference pref = new CheckBoxPreference(getInstrumentation().getContext());
+    pref.setKey("foo");
+    assertThat(pref, withKey("foo"));
+    assertThat(pref, not(withKey("bar")));
+    assertThat(pref, withKey(is("foo")));
+  }
+}
diff --git a/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/matcher/ViewMatchersTest.java b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/matcher/ViewMatchersTest.java
new file mode 100644
index 0000000..5000e46
--- /dev/null
+++ b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/matcher/ViewMatchersTest.java
@@ -0,0 +1,456 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.matcher;
+
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.hasContentDescription;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.hasDescendant;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.hasImeAction;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.hasSibling;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isAssignableFrom;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isChecked;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isClickable;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isDescendantOfA;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isEnabled;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isFocusable;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isNotChecked;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isRoot;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.supportsInputMethods;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withChild;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withContentDescription;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withEffectiveVisibility;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withParent;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withTagKey;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withTagValue;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withText;
+import static org.hamcrest.Matchers.is;
+
+import com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.Visibility;
+import com.google.android.apps.common.testing.ui.testapp.test.R;
+
+import android.test.InstrumentationTestCase;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.EditorInfo;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.Checkable;
+import android.widget.CheckedTextView;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.RadioButton;
+import android.widget.RelativeLayout;
+import android.widget.ScrollView;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import org.hamcrest.Matcher;
+import org.hamcrest.Matchers;
+
+/**
+ * Unit tests for {@link ViewMatchers}.
+ */
+public class ViewMatchersTest extends InstrumentationTestCase {
+  public void testIsAssignableFrom_notAnInstance() {
+    View v = new View(getInstrumentation().getTargetContext());
+    assertFalse(isAssignableFrom(Spinner.class).matches(v));
+  }
+
+  public void testIsAssignableFrom_plainView() {
+    View v = new View(getInstrumentation().getTargetContext());
+    assertTrue(isAssignableFrom(View.class).matches(v));
+  }
+
+  public void testIsAssignableFrom_superclass() {
+    View v = new RadioButton(getInstrumentation().getTargetContext());
+    assertTrue(isAssignableFrom(Button.class).matches(v));
+  }
+
+  @SuppressWarnings("cast")
+  public void testWithContentDescriptionCharSequence() {
+    View view = new View(getInstrumentation().getTargetContext());
+    view.setContentDescription(null);
+    assertTrue(withContentDescription(Matchers.<CharSequence>nullValue()).matches(view));
+    CharSequence testText = "test text!";
+    view.setContentDescription(testText);
+    assertTrue(withContentDescription(is(testText)).matches(view));
+    assertFalse(withContentDescription(is((CharSequence) "blah")).matches(view));
+    assertFalse(withContentDescription(is((CharSequence) "")).matches(view));
+  }
+
+  public void testWithContentDescriptionNull() {
+    try {
+      withContentDescription((Matcher<CharSequence>) null);
+      fail("Should of thrown NPE");
+    } catch (NullPointerException e) {
+      // Good, this is expected.
+    }
+  }
+
+  public void testHasContentDescription() {
+    View view = new View(getInstrumentation().getTargetContext());
+    view.setContentDescription(null);
+    assertFalse(hasContentDescription().matches(view));
+    CharSequence testText = "test text!";
+    view.setContentDescription(testText);
+    assertTrue(hasContentDescription().matches(view));
+  }
+
+  public void testWithContentDescriptionString() {
+    View view = new View(getInstrumentation().getTargetContext());
+    view.setContentDescription(null);
+    assertTrue(withContentDescription(Matchers.<String>nullValue()).matches(view));
+    String testText = "test text!";
+    view.setContentDescription(testText);
+    assertTrue(withContentDescription(is(testText)).matches(view));
+    assertFalse(withContentDescription(is("blah")).matches(view));
+    assertFalse(withContentDescription(is("")).matches(view));
+  }
+
+  public void testWithId() {
+    View view = new View(getInstrumentation().getTargetContext());
+    view.setId(R.id.testId1);
+    assertTrue(withId(is(R.id.testId1)).matches(view));
+    assertFalse(withId(is(R.id.testId2)).matches(view));
+    assertFalse(withId(is(1234)).matches(view));
+  }
+
+  public void testWithTagNull() {
+    try {
+      withTagKey(0, null);
+      fail("Should of thrown NPE");
+    } catch (NullPointerException e) {
+      // Good, this is expected.
+    }
+
+    try {
+      withTagValue(null);
+      fail("Should of thrown NPE");
+    } catch (NullPointerException e) {
+      // Good, this is expected.
+    }
+  }
+
+  public void testWithTagObject() {
+    View view = new View(getInstrumentation().getTargetContext());
+    view.setTag(null);
+    assertTrue(withTagValue(Matchers.<Object>nullValue()).matches(view));
+    String testObjectText = "test text!";
+    view.setTag(testObjectText);
+    assertFalse(withTagKey(R.id.testId1).matches(view));
+    assertTrue(withTagValue(is((Object) testObjectText)).matches(view));
+    assertFalse(withTagValue(is((Object) "blah")).matches(view));
+    assertFalse(withTagValue(is((Object) "")).matches(view));
+  }
+
+  public void testWithTagKey() {
+    View view = new View(getInstrumentation().getTargetContext());
+    assertFalse(withTagKey(R.id.testId1).matches(view));
+    view.setTag(R.id.testId1, "blah");
+    assertFalse(withTagValue(is((Object) "blah")).matches(view));
+    assertTrue(withTagKey(R.id.testId1).matches(view));
+    assertFalse(withTagKey(R.id.testId2).matches(view));
+    assertFalse(withTagKey(R.id.testId3).matches(view));
+    assertFalse(withTagKey(65535).matches(view));
+
+    view.setTag(R.id.testId2, "blah2");
+    assertTrue(withTagKey(R.id.testId1).matches(view));
+    assertTrue(withTagKey(R.id.testId2).matches(view));
+    assertFalse(withTagKey(R.id.testId3).matches(view));
+    assertFalse(withTagKey(65535).matches(view));
+    assertFalse(withTagValue(is((Object) "blah")).matches(view));
+  }
+
+  public void testWithTagKeyObject() {
+    View view = new View(getInstrumentation().getTargetContext());
+    String testObjectText1 = "test text1!";
+    String testObjectText2 = "test text2!";
+    assertFalse(withTagKey(R.id.testId1, is((Object) testObjectText1)).matches(view));
+    view.setTag(R.id.testId1, testObjectText1);
+    assertTrue(withTagKey(R.id.testId1, is((Object) testObjectText1)).matches(view));
+    assertFalse(withTagKey(R.id.testId1, is((Object) testObjectText2)).matches(view));
+    assertFalse(withTagKey(R.id.testId2, is((Object) testObjectText1)).matches(view));
+    assertFalse(withTagKey(R.id.testId3, is((Object) testObjectText1)).matches(view));
+    assertFalse(withTagKey(65535, is((Object) testObjectText1)).matches(view));
+    assertFalse(withTagValue(is((Object) "blah")).matches(view));
+
+    view.setTag(R.id.testId2, testObjectText2);
+    assertTrue(withTagKey(R.id.testId1, is((Object) testObjectText1)).matches(view));
+    assertFalse(withTagKey(R.id.testId1, is((Object) testObjectText2)).matches(view));
+    assertTrue(withTagKey(R.id.testId2, is((Object) testObjectText2)).matches(view));
+    assertFalse(withTagKey(R.id.testId2, is((Object) testObjectText1)).matches(view));
+    assertFalse(withTagKey(R.id.testId3, is((Object) testObjectText1)).matches(view));
+    assertFalse(withTagKey(65535, is((Object) testObjectText1)).matches(view));
+    assertFalse(withTagValue(is((Object) "blah")).matches(view));
+  }
+
+  public void testWithTextNull() {
+    try {
+      withText((Matcher<String>) null);
+      fail("Should of thrown NPE");
+    } catch (NullPointerException e) {
+      // Good, this is expected.
+    }
+  }
+
+  public void testCheckBoxMatchers() {
+    assertFalse(isChecked().matches(new Spinner(getInstrumentation().getTargetContext())));
+    assertFalse(isNotChecked().matches(new Spinner(getInstrumentation().getTargetContext())));
+
+    CheckBox checkBox = new CheckBox(getInstrumentation().getTargetContext());
+    checkBox.setChecked(true);
+    assertTrue(isChecked().matches(checkBox));
+    assertFalse(isNotChecked().matches(checkBox));
+
+    checkBox.setChecked(false);
+    assertFalse(isChecked().matches(checkBox));
+    assertTrue(isNotChecked().matches(checkBox));
+
+    RadioButton radioButton = new RadioButton(getInstrumentation().getTargetContext());
+    radioButton.setChecked(false);
+    assertFalse(isChecked().matches(radioButton));
+    assertTrue(isNotChecked().matches(radioButton));
+
+    radioButton.setChecked(true);
+    assertTrue(isChecked().matches(radioButton));
+    assertFalse(isNotChecked().matches(radioButton));
+
+    CheckedTextView checkedText = new CheckedTextView(getInstrumentation().getTargetContext());
+    checkedText.setChecked(false);
+    assertFalse(isChecked().matches(checkedText));
+    assertTrue(isNotChecked().matches(checkedText));
+
+    checkedText.setChecked(true);
+    assertTrue(isChecked().matches(checkedText));
+    assertFalse(isNotChecked().matches(checkedText));
+
+    Checkable checkable = new Checkable() {
+      @Override
+      public boolean isChecked() { return true; }
+      @Override
+      public void setChecked(boolean ignored) {}
+      @Override
+      public void toggle() {}
+    };
+
+    assertFalse(isChecked().matches(checkable));
+    assertFalse(isNotChecked().matches(checkable));
+  }
+
+  public void testWithTextString() {
+    TextView textView = new TextView(getInstrumentation().getTargetContext());
+    textView.setText(null);
+    assertTrue(withText(is("")).matches(textView));
+    String testText = "test text!";
+    textView.setText(testText);
+    assertTrue(withText(is(testText)).matches(textView));
+    assertFalse(withText(is("blah")).matches(textView));
+    assertFalse(withText(is("")).matches(textView));
+  }
+
+  public void testHasDescendant() {
+    View v = new TextView(getInstrumentation().getTargetContext());
+    ViewGroup parent = new RelativeLayout(getInstrumentation().getTargetContext());
+    ViewGroup grany = new ScrollView(getInstrumentation().getTargetContext());
+    grany.addView(parent);
+    parent.addView(v);
+    assertTrue(hasDescendant(isAssignableFrom(TextView.class)).matches(grany));
+    assertTrue(hasDescendant(isAssignableFrom(TextView.class)).matches(parent));
+    assertFalse(hasDescendant(isAssignableFrom(ScrollView.class)).matches(parent));
+    assertFalse(hasDescendant(isAssignableFrom(TextView.class)).matches(v));
+  }
+
+  public void testIsDescendantOfA() {
+    View v = new TextView(getInstrumentation().getTargetContext());
+    ViewGroup parent = new RelativeLayout(getInstrumentation().getTargetContext());
+    ViewGroup grany = new ScrollView(getInstrumentation().getTargetContext());
+    grany.addView(parent);
+    parent.addView(v);
+    assertTrue(isDescendantOfA(isAssignableFrom(RelativeLayout.class)).matches(v));
+    assertTrue(isDescendantOfA(isAssignableFrom(ScrollView.class)).matches(v));
+    assertFalse(isDescendantOfA(isAssignableFrom(LinearLayout.class)).matches(v));
+  }
+
+  public void testIsVisible() {
+    View visible = new View(getInstrumentation().getTargetContext());
+    visible.setVisibility(View.VISIBLE);
+    View invisible = new View(getInstrumentation().getTargetContext());
+    invisible.setVisibility(View.INVISIBLE);
+    assertTrue(withEffectiveVisibility(Visibility.VISIBLE).matches(visible));
+    assertFalse(withEffectiveVisibility(Visibility.VISIBLE).matches(invisible));
+
+    // Make the visible view invisible by giving it an invisible parent.
+    ViewGroup parent = new RelativeLayout(getInstrumentation().getTargetContext());
+    parent.addView(visible);
+    parent.setVisibility(View.INVISIBLE);
+    assertFalse(withEffectiveVisibility(Visibility.VISIBLE).matches(visible));
+  }
+
+  public void testIsInvisible() {
+    View visible = new View(getInstrumentation().getTargetContext());
+    visible.setVisibility(View.VISIBLE);
+    View invisible = new View(getInstrumentation().getTargetContext());
+    invisible.setVisibility(View.INVISIBLE);
+    assertFalse(withEffectiveVisibility(Visibility.INVISIBLE).matches(visible));
+    assertTrue(withEffectiveVisibility(Visibility.INVISIBLE).matches(invisible));
+
+    // Make the visible view invisible by giving it an invisible parent.
+    ViewGroup parent = new RelativeLayout(getInstrumentation().getTargetContext());
+    parent.addView(visible);
+    parent.setVisibility(View.INVISIBLE);
+    assertTrue(withEffectiveVisibility(Visibility.INVISIBLE).matches(visible));
+  }
+
+  public void testIsGone() {
+    View gone = new View(getInstrumentation().getTargetContext());
+    gone.setVisibility(View.GONE);
+    View visible = new View(getInstrumentation().getTargetContext());
+    visible.setVisibility(View.VISIBLE);
+    assertFalse(withEffectiveVisibility(Visibility.GONE).matches(visible));
+    assertTrue(withEffectiveVisibility(Visibility.GONE).matches(gone));
+
+    // Make the gone view gone by giving it a gone parent.
+    ViewGroup parent = new RelativeLayout(getInstrumentation().getTargetContext());
+    parent.addView(visible);
+    parent.setVisibility(View.GONE);
+    assertTrue(withEffectiveVisibility(Visibility.GONE).matches(visible));
+  }
+
+  public void testIsClickable() {
+    View clickable = new View(getInstrumentation().getTargetContext());
+    clickable.setClickable(true);
+    View notClickable = new View(getInstrumentation().getTargetContext());
+    notClickable.setClickable(false);
+    assertTrue(isClickable().matches(clickable));
+    assertFalse(isClickable().matches(notClickable));
+  }
+
+  public void testIsEnabled() {
+    View enabled = new View(getInstrumentation().getTargetContext());
+    enabled.setEnabled(true);
+    View notEnabled = new View(getInstrumentation().getTargetContext());
+    notEnabled.setEnabled(false);
+    assertTrue(isEnabled().matches(enabled));
+    assertFalse(isEnabled().matches(notEnabled));
+  }
+
+  public void testIsFocusable() {
+    View focusable = new View(getInstrumentation().getTargetContext());
+    focusable.setFocusable(true);
+    View notFocusable = new View(getInstrumentation().getTargetContext());
+    notFocusable.setFocusable(false);
+    assertTrue(isFocusable().matches(focusable));
+    assertFalse(isFocusable().matches(notFocusable));
+  }
+
+  public void testWithTextResourceId() {
+    TextView textView = new TextView(getInstrumentation().getTargetContext());
+    textView.setText(R.string.something);
+    assertTrue(withText(R.string.something).matches(textView));
+    assertFalse(withText(R.string.other_string).matches(textView));
+  }
+
+  public void testWithParent() {
+    View view1 = new TextView(getInstrumentation().getTargetContext());
+    View view2 = new TextView(getInstrumentation().getTargetContext());
+    View view3 = new TextView(getInstrumentation().getTargetContext());
+    ViewGroup tiptop = new RelativeLayout(getInstrumentation().getTargetContext());
+    ViewGroup secondLevel = new RelativeLayout(getInstrumentation().getTargetContext());
+    secondLevel.addView(view2);
+    secondLevel.addView(view3);
+    tiptop.addView(secondLevel);
+    tiptop.addView(view1);
+    assertTrue(withParent(is((View) tiptop)).matches(view1));
+    assertTrue(withParent(is((View) tiptop)).matches(secondLevel));
+    assertFalse(withParent(is((View) tiptop)).matches(view2));
+    assertFalse(withParent(is((View) tiptop)).matches(view3));
+    assertFalse(withParent(is((View) secondLevel)).matches(view1));
+
+    assertTrue(withParent(is((View) secondLevel)).matches(view2));
+    assertTrue(withParent(is((View) secondLevel)).matches(view3));
+
+    assertFalse(withParent(is(view3)).matches(view3));
+  }
+
+  public void testWithChild() {
+    View view1 = new TextView(getInstrumentation().getTargetContext());
+    View view2 = new TextView(getInstrumentation().getTargetContext());
+    View view3 = new TextView(getInstrumentation().getTargetContext());
+    ViewGroup tiptop = new RelativeLayout(getInstrumentation().getTargetContext());
+    ViewGroup secondLevel = new RelativeLayout(getInstrumentation().getTargetContext());
+    secondLevel.addView(view2);
+    secondLevel.addView(view3);
+    tiptop.addView(secondLevel);
+    tiptop.addView(view1);
+    assertTrue(withChild(is(view1)).matches(tiptop));
+    assertTrue(withChild(is((View) secondLevel)).matches(tiptop));
+    assertFalse(withChild(is((View) tiptop)).matches(view1));
+    assertFalse(withChild(is(view2)).matches(tiptop));
+    assertFalse(withChild(is(view1)).matches(secondLevel));
+
+    assertTrue(withChild(is(view2)).matches(secondLevel));
+
+    assertFalse(withChild(is(view3)).matches(view3));
+  }
+
+  public void testIsRootView() {
+    ViewGroup rootView = new ViewGroup(getInstrumentation().getTargetContext()) {
+      @Override
+      protected void onLayout(boolean changed, int l, int t, int r, int b) {
+      }
+    };
+
+    View view = new View(getInstrumentation().getTargetContext());
+    rootView.addView(view);
+
+    assertTrue(isRoot().matches(rootView));
+    assertFalse(isRoot().matches(view));
+  }
+
+  public void testHasSibling() {
+    TextView v1 = new TextView(getInstrumentation().getTargetContext());
+    v1.setText("Bill Odama");
+    Button v2 = new Button(getInstrumentation().getTargetContext());
+    View v3 = new View(getInstrumentation().getTargetContext());
+    ViewGroup parent = new LinearLayout(getInstrumentation().getTargetContext());
+    parent.addView(v1);
+    parent.addView(v2);
+    parent.addView(v3);
+    assertTrue(hasSibling(withText("Bill Odama")).matches(v2));
+    assertFalse(hasSibling(is(v3)).matches(parent));
+  }
+
+  public void testHasImeAction() {
+    EditText editText = new EditText(getInstrumentation().getTargetContext());
+    assertFalse(hasImeAction(EditorInfo.IME_ACTION_GO).matches(editText));
+    editText.setImeOptions(EditorInfo.IME_ACTION_NEXT);
+    assertFalse(hasImeAction(EditorInfo.IME_ACTION_GO).matches(editText));
+    assertTrue(hasImeAction(EditorInfo.IME_ACTION_NEXT).matches(editText));
+  }
+
+  public void testHasImeActionNoInputConnection() {
+    Button button = new Button(getInstrumentation().getTargetContext());
+    assertFalse(hasImeAction(0).matches(button));
+  }
+
+  public void testSupportsInputMethods() {
+    Button button = new Button(getInstrumentation().getTargetContext());
+    EditText editText = new EditText(getInstrumentation().getTargetContext());
+    assertFalse(supportsInputMethods().matches(button));
+    assertTrue(supportsInputMethods().matches(editText));
+  }
+}
diff --git a/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/util/TreeIterablesTest.java b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/util/TreeIterablesTest.java
new file mode 100644
index 0000000..9b2bdcc
--- /dev/null
+++ b/espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/util/TreeIterablesTest.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.util;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.hasEntry;
+import static org.hamcrest.Matchers.is;
+
+import com.google.android.apps.common.testing.ui.espresso.util.TreeIterables.DistanceRecordingTreeViewer;
+import com.google.android.apps.common.testing.ui.espresso.util.TreeIterables.TreeViewer;
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+
+import junit.framework.TestCase;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/** Unit tests for {@link TreeIterables}. */
+public class TreeIterablesTest extends TestCase {
+
+  private static class TestElement {
+    private final String data;
+    private final ImmutableList<TestElement> children;
+    public TestElement(String data, TestElement ... children) {
+      this.data = checkNotNull(data);
+      this.children = ImmutableList.copyOf(children);
+    }
+  }
+
+  private static class TestElementTreeViewer implements TreeViewer<TestElement> {
+    @Override
+    public Collection<TestElement> children(TestElement element) {
+      return element.children;
+    }
+  }
+
+  private static class TestElementStringConvertor implements Function<TestElement, String> {
+    @Override
+    public String apply(TestElement e) {
+      return e.data;
+    }
+  }
+
+
+  private static final TestElement trivialTree =
+      new TestElement("a", new TestElement("b", new TestElement("c", new TestElement("d"))));
+
+  private static final TestElement complexTree =
+      new TestElement("a",
+        new TestElement("b",
+          new TestElement("c",
+            new TestElement("d"),
+            new TestElement("e",
+              new TestElement("f"))),
+          new TestElement("g"),
+          new TestElement("h",
+            new TestElement("i",
+              new TestElement("j",
+                new TestElement("k"))))),
+        new TestElement("l"),
+        new TestElement("m"),
+        new TestElement("n",
+                        new TestElement("o",
+            new TestElement("p"),
+            new TestElement("q"))));
+
+  public void testDistanceRecorder_unknownItemThrowsException() {
+    final DistanceRecordingTreeViewer<TestElement> distanceRecorder =
+        new DistanceRecordingTreeViewer<TestElement>(complexTree, new TestElementTreeViewer());
+    try {
+      distanceRecorder.getDistance(new TestElement("hello"));
+      fail("node should be unknown");
+    } catch (RuntimeException expected) { }
+  }
+
+  public void testDistanceRecorder_unprocessedChildThrowsException() {
+    final DistanceRecordingTreeViewer<TestElement> distanceRecorder =
+        new DistanceRecordingTreeViewer<TestElement>(complexTree, new TestElementTreeViewer());
+
+    try {
+      distanceRecorder.getDistance(complexTree.children.iterator().next());
+      fail("distance recorder hasnt processed this child yet, cannot know distance");
+    } catch (RuntimeException expected) { }
+  }
+
+  public void testDistanceRecorder_distanceKnownAfterChildrenCall() {
+    final DistanceRecordingTreeViewer<TestElement> distanceRecorder =
+        new DistanceRecordingTreeViewer<TestElement>(complexTree, new TestElementTreeViewer());
+
+    @SuppressWarnings("unused")
+    List<TestElement> createdForSideEffect = Lists.newArrayList(
+        distanceRecorder.children(complexTree));
+
+    assertThat(distanceRecorder.getDistance(complexTree), is(0));
+    assertThat(distanceRecorder.getDistance(complexTree.children.iterator().next()), is(1));
+  }
+
+  @SuppressWarnings("unchecked")
+  public void testComplexTree_Distances() {
+    final DistanceRecordingTreeViewer<TestElement> distanceRecorder =
+        new DistanceRecordingTreeViewer<TestElement>(complexTree, new TestElementTreeViewer());
+    Iterable<TestElement> complexIterable = TreeIterables.depthFirstTraversal(complexTree,
+        distanceRecorder);
+    Set<TestElement> complexSet = Sets.newHashSet(complexIterable);
+    Map<String, Integer> distancesByData = Maps.newHashMap();
+    for (TestElement e : complexSet) {
+      distancesByData.put(e.data, distanceRecorder.getDistance(e));
+    }
+
+    assertThat(distancesByData, allOf(
+        hasEntry("a", 0),
+        hasEntry("b", 1),
+        hasEntry("c", 2),
+        hasEntry("d", 3),
+        hasEntry("e", 3),
+        hasEntry("f", 4),
+        hasEntry("g", 2),
+        hasEntry("h", 2),
+        hasEntry("i", 3),
+        hasEntry("j", 4),
+        hasEntry("k", 5),
+        hasEntry("l", 1),
+        hasEntry("m", 1),
+        hasEntry("n", 1),
+        hasEntry("o", 2),
+        hasEntry("p", 3),
+        hasEntry("q", 3)));
+    assertThat(distancesByData.size(), is(17));
+
+    List<String> traversalOrder = Lists.newArrayList(Iterables.transform(
+        complexIterable,
+        new TestElementStringConvertor()));
+
+    // should be depth first if forwarding correctly.
+    assertThat(traversalOrder,
+        is((List<String>) Lists.newArrayList(
+            "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q")));
+  }
+
+  public void testComplexTraversal_depthFirst() {
+    List<String> breadthFirst = Lists.newArrayList(Iterables.transform(
+        TreeIterables.depthFirstTraversal(complexTree, new TestElementTreeViewer()),
+        new TestElementStringConvertor()));
+    assertThat(breadthFirst,
+        is((Iterable<String>) Lists.newArrayList(
+            "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q")));
+  }
+
+  public void testComplexTraversal_breadthFirst() {
+    List<String> breadthFirst = Lists.newArrayList(Iterables.transform(
+        TreeIterables.breadthFirstTraversal(complexTree, new TestElementTreeViewer()),
+        new TestElementStringConvertor()));
+    assertThat(breadthFirst,
+        is((List<String>) Lists.newArrayList(
+            "a", //root
+            "b", "l", "m", "n", //L1
+            "c", "g", "h", "o", //L2
+            "d", "e", "i", "p", "q", //L3
+            "f", "j", // L4
+            "k"))); //L5
+  }
+
+  public void testTrivialTraversal_breadthFirst() {
+    // essentially the same as depth first.
+    List<String> breadthFirst = Lists.newArrayList(Iterables.transform(
+        TreeIterables.breadthFirstTraversal(trivialTree, new TestElementTreeViewer()),
+        new TestElementStringConvertor()));
+    assertThat(breadthFirst, is((List<String>) Lists.newArrayList("a", "b", "c", "d")));
+  }
+
+  public void testTrivialTraversal_depthFirst() {
+    List<String> depthFirst = Lists.newArrayList(Iterables.transform(
+        TreeIterables.depthFirstTraversal(trivialTree, new TestElementTreeViewer()),
+        new TestElementStringConvertor()));
+    assertThat(depthFirst, is((List<String>) Lists.newArrayList("a", "b", "c", "d")));
+  }
+
+  @SuppressWarnings("unchecked")
+  public void testTrivial_distance() {
+    final DistanceRecordingTreeViewer<TestElement> distanceRecorder =
+        new DistanceRecordingTreeViewer<TestElement>(trivialTree, new TestElementTreeViewer());
+
+    Iterable<TestElement> trivialIterable = TreeIterables.depthFirstTraversal(trivialTree,
+        distanceRecorder);
+    Set<TestElement> trivialSet = Sets.newHashSet(trivialIterable);
+    Map<String, Integer> distancesByData = Maps.newHashMap();
+    for (TestElement e : trivialSet) {
+      distancesByData.put(e.data, distanceRecorder.getDistance(e));
+    }
+
+    assertThat(distancesByData, allOf(
+        hasEntry("a", 0),
+        hasEntry("b", 1),
+        hasEntry("c", 2),
+        hasEntry("d", 3)));
+    assertThat(distancesByData.size(), is(4));
+  }
+}
diff --git a/espresso/espresso-lib-tests/src/androidTest/res/values/id.xml b/espresso/espresso-lib-tests/src/androidTest/res/values/id.xml
new file mode 100644
index 0000000..62358e3
--- /dev/null
+++ b/espresso/espresso-lib-tests/src/androidTest/res/values/id.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2014 The Android Open Source Project
+  ~
+  ~ 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.
+  -->
+
+<resources>
+  <!-- IDs used for testing purposes -->
+  <item type="id" name="testId1" />
+  <item type="id" name="testId2" />
+  <item type="id" name="testId3" />
+</resources>
diff --git a/espresso/espresso-lib-tests/src/androidTest/res/values/strings.xml b/espresso/espresso-lib-tests/src/androidTest/res/values/strings.xml
new file mode 100644
index 0000000..54a4ecc
--- /dev/null
+++ b/espresso/espresso-lib-tests/src/androidTest/res/values/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+  ~ Copyright (C) 2014 The Android Open Source Project
+  ~
+  ~ 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.
+  -->
+
+<resources>
+  <string name="something">Hello World</string>
+  <string name="other_string">Goodbye!!</string>
+</resources>
diff --git a/espresso/espresso-lib/build.gradle b/espresso/espresso-lib/build.gradle
new file mode 100644
index 0000000..b02908f
--- /dev/null
+++ b/espresso/espresso-lib/build.gradle
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * 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.
+ */
+
+apply plugin: 'android-library'
+
+sourceCompatibility = JavaVersion.VERSION_1_5
+targetCompatibility = JavaVersion.VERSION_1_5
+
+repositories {
+    maven { url '../../../../prebuilts/tools/common/m2/repository' }
+    maven { url '../../../../prebuilts/tools/common/m2/internal' }
+}
+
+android {
+    compileSdkVersion 19
+    buildToolsVersion '19.0.3'
+
+    // to overwrite debug variant
+    publishNonDefault true
+
+    lintOptions {
+        abortOnError false
+    }
+
+    packagingOptions {
+        exclude 'LICENSE.txt'
+    }
+}
+
+// create separate scope for jarjar
+configurations {
+    jarjar
+}
+
+dependencies {
+    // set to provided since we're manually adding the JarJar'd version
+    provided files('../libs/dagger-1.2.1.jar')
+    provided files('../libs/dagger-compiler-1.2.1.jar')
+    provided files('../libs/guava-14.0.1.jar')
+
+    compile project(':idling-resource-interface')
+    compile 'javax.annotation:javax.annotation-api:1.2'
+    compile 'javax.inject:javax.inject:1'
+    compile 'com.google.code.findbugs:jsr305:2.0.1'
+    compile 'org.hamcrest:hamcrest-library:1.1'
+    compile 'org.hamcrest:hamcrest-integration:1.1'
+    compile 'org.hamcrest:hamcrest-core:1.1'
+    compile 'com.squareup:javawriter:2.1.1'
+
+    jarjar files('../libs/jarjar-1.4.jar')
+
+    // Temporarily include the Google3 TestRunner as a static jar
+    // until it's merged with the Android one.
+    compile files('../libs/testrunner-runtime-1.1.jar')
+    compile files('../libs/testrunner-1.1.jar')
+}
+
+android.libraryVariants.all { variant ->
+
+    // To run unit tests against un-jarjar version of the lib.
+    if (variant.buildType.name.equals(com.android.builder.BuilderConstants.DEBUG)) {
+        println "Skipping debug build type."
+        return;
+    }
+
+    def classesJar = "$project.buildDir/bundles/$variant.dirName/classes.jar"
+    def tmpClassesJarDir = "$project.buildDir/pre-jarjar/$variant.dirName"
+    def tmpClassesJar = "$tmpClassesJarDir/classes.jar"
+
+    def depDaggerJar = "../libs/dagger-1.2.1.jar"
+    def depGuavaJar = "../libs/guava-14.0.1.jar"
+    def jarJarTaskName = "jarJar${variant.name.capitalize()}"
+
+    task "$jarJarTaskName" << {
+        project.ant {
+            taskdef name: "jarjar", classname: "com.tonicsystems.jarjar.JarJarTask",
+                    classpath: configurations.jarjar.asPath
+            jarjar(jarfile: "$classesJar", filesetmanifest: "merge") {
+                zipfileset(src: "$depGuavaJar")
+                zipfileset(src: "$depDaggerJar")
+                zipfileset(src: "$tmpClassesJar")
+                rule pattern: "com.google.common.**",
+                        result: "com.google.android.apps.common.testing.deps.guava.@1"
+                rule pattern: "dagger.**",
+                        result: "com.google.android.apps.common.testing.deps.dagger.@1"
+            }
+        }
+    }
+
+    // get access to the normal jar class. Change its output to somewhere else, and make jarjar depend on it.
+    Jar classesJarTask = (Jar) project.tasks.getByName("package${variant.name.capitalize()}Jar")
+
+    classesJarTask.destinationDir = project.file("$tmpClassesJarDir")
+    project.tasks.getByName("$jarJarTaskName").dependsOn classesJarTask, configurations.provided
+
+    variant.packageLibrary.dependsOn "$jarJarTaskName"
+}
+
+apply from: '../publishLocal.gradle'
diff --git a/espresso/espresso-lib/gradle.properties b/espresso/espresso-lib/gradle.properties
new file mode 100644
index 0000000..df3f7c9
--- /dev/null
+++ b/espresso/espresso-lib/gradle.properties
@@ -0,0 +1,21 @@
+#
+# Copyright (C) 2014 The Android Open Source Project
+#
+# 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.
+#
+
+VERSION=1.2
+POM_NAME=Espresso Library
+GROUP_ID=com.google.android.apps.common.testing
+POM_ARTIFACT_ID=espresso-lib
+POM_PACKAGING=aar
\ No newline at end of file
diff --git a/espresso/espresso-lib/src/main/AndroidManifest.xml b/espresso/espresso-lib/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..e6813a3
--- /dev/null
+++ b/espresso/espresso-lib/src/main/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2014 The Android Open Source Project
+  ~
+  ~ 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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.google.android.apps.common.testing.ui.espresso" >
+
+    <uses-sdk
+        android:minSdkVersion="7"/>
+
+    <application />
+
+</manifest>
\ No newline at end of file
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/AmbiguousViewMatcherException.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/AmbiguousViewMatcherException.java
new file mode 100644
index 0000000..41c3678
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/AmbiguousViewMatcherException.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.android.apps.common.testing.ui.espresso.util.HumanReadables;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+
+import android.view.View;
+
+import org.hamcrest.Matcher;
+
+/**
+ * An exception which indicates that a Matcher<View> matched multiple views in the hierarchy when
+ * only one view was expected. It should be called only from the main thread.
+ * <p>
+ * Contains details about the matcher and the current view hierarchy to aid in debugging.
+ * </p>
+ * <p>
+ * Since this is usually an unrecoverable error this exception is a runtime exception.
+ * </p>
+ * <p>
+ * References to the view and failing matcher are purposefully not included in the state of this
+ * object - since it will most likely be created on the UI thread and thrown on the instrumentation
+ * thread, it would be invalid to touch the view on the instrumentation thread. Also the view
+ * hierarchy may have changed since exception creation (leading to more confusion).
+ * </p>
+ */
+public final class AmbiguousViewMatcherException extends RuntimeException
+    implements EspressoException {
+
+  private Matcher<? super View> viewMatcher;
+  private View rootView;
+  private View view1;
+  private View view2;
+  private View[] others;
+
+  private AmbiguousViewMatcherException(String description) {
+    super(description);
+  }
+
+  private AmbiguousViewMatcherException(Builder builder) {
+    super(getErrorMessage(builder));
+    this.viewMatcher = builder.viewMatcher;
+    this.rootView = builder.rootView;
+    this.view1 = builder.view1;
+    this.view2 = builder.view2;
+    this.others = builder.others;
+  }
+
+  private static String getErrorMessage(Builder builder) {
+    String errorMessage = "";
+    if (builder.includeViewHierarchy) {
+      ImmutableSet<View> ambiguousViews =
+        ImmutableSet.<View>builder().add(builder.view1, builder.view2).add(builder.others).build();
+      errorMessage = HumanReadables.getViewHierarchyErrorMessage(builder.rootView,
+          Lists.newArrayList(ambiguousViews),
+          String.format("'%s' matches multiple views in the hierarchy.", builder.viewMatcher),
+          "****MATCHES****");
+    } else {
+      errorMessage = String.format("Multiple Ambiguous Views found for matcher %s",
+          builder.viewMatcher);
+    }
+    return errorMessage;
+  }
+
+  /** Builder for {@link AmbiguousViewMatcherException}. */
+  public static class Builder {
+    private Matcher<? super View> viewMatcher;
+    private View rootView;
+    private View view1;
+    private View view2;
+    private View[] others;
+    private boolean includeViewHierarchy = true;
+
+    public Builder from(AmbiguousViewMatcherException exception) {
+      this.viewMatcher = exception.viewMatcher;
+      this.rootView = exception.rootView;
+      this.view1 = exception.view1;
+      this.view2 = exception.view2;
+      this.others = exception.others;
+      return this;
+    }
+
+    public Builder withViewMatcher(Matcher<? super View> viewMatcher) {
+      this.viewMatcher = viewMatcher;
+      return this;
+    }
+
+    public Builder withRootView(View rootView) {
+      this.rootView = rootView;
+      return this;
+    }
+
+    public Builder withView1(View view1) {
+      this.view1 = view1;
+      return this;
+    }
+
+    public Builder withView2(View view2) {
+      this.view2 = view2;
+      return this;
+    }
+
+    public Builder withOtherAmbiguousViews(View... others) {
+      this.others = others;
+      return this;
+    }
+
+    public Builder includeViewHierarchy(boolean includeViewHierarchy) {
+      this.includeViewHierarchy = includeViewHierarchy;
+      return this;
+    }
+
+    public AmbiguousViewMatcherException build() {
+      checkNotNull(viewMatcher);
+      checkNotNull(rootView);
+      checkNotNull(view1);
+      checkNotNull(view2);
+      checkNotNull(others);
+      return new AmbiguousViewMatcherException(this);
+    }
+  }
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/AndroidManifest.xml b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/AndroidManifest.xml
new file mode 100644
index 0000000..e32c392
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2014 The Android Open Source Project
+  ~
+  ~ 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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.google.android.apps.common.testing.ui.espresso" >
+
+    <uses-sdk
+        android:minSdkVersion="7"
+        android:targetSdkVersion="17" />
+
+    <application android:label="Espresso" />
+
+</manifest>
\ No newline at end of file
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/AppNotIdleException.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/AppNotIdleException.java
new file mode 100644
index 0000000..e84fcca
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/AppNotIdleException.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.base.Joiner;
+
+import android.os.Looper;
+
+import java.util.List;
+
+/**
+ * An exception which indicates that the App has not become idle even after the specified duration.
+ */
+public final class AppNotIdleException extends RuntimeException implements EspressoException {
+
+  private AppNotIdleException(String description) {
+    super(description);
+  }
+
+  /**
+   * Creates a new AppNotIdleException suitable for erroring out a test case.
+   *
+   * This should be called only from the main thread if the app does not idle out within the
+   * specified duration.
+   *
+   * @param idleConditions list of idleConditions that failed to become idle.
+   * @param loopCount number of times it was tried to check if they became idle.
+   * @param seconds number of seconds that was tried before giving up.
+   *
+   * @return a AppNotIdleException suitable to be thrown on the instrumentation thread.
+   */
+  @Deprecated
+  public static AppNotIdleException create(List<String> idleConditions, int loopCount,
+      int seconds) {
+    checkState(Looper.myLooper() == Looper.getMainLooper());
+    String errorMessage = String.format("App not idle within timeout of %s seconds even" +
+        "after trying for %s iterations. The following Idle Conditions failed %s",
+        seconds, loopCount, Joiner.on(",").join(idleConditions));
+    return new AppNotIdleException(errorMessage);
+  }
+
+  /**
+   * Creates a new AppNotIdleException suitable for erroring out a test case.
+   *
+   * This should be called only from the main thread if the app does not idle out within the
+   * specified duration.
+   *
+   * @param idleConditions list of idleConditions that failed to become idle.
+   * @param message a message about the failure.
+   *
+   * @return a AppNotIdleException suitable to be thrown on the instrumentation thread.
+   */
+  static AppNotIdleException create(List<String> idleConditions, String message) {
+    String errorMessage = String.format("%s The following Idle Conditions failed %s.",
+        message, Joiner.on(",").join(idleConditions));
+    return new AppNotIdleException(errorMessage);
+  }
+}
+
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/DataInteraction.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/DataInteraction.java
new file mode 100644
index 0000000..aa13d5e
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/DataInteraction.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso;
+
+
+import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isAssignableFrom;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isDescendantOfA;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.hamcrest.Matchers.allOf;
+
+import com.google.android.apps.common.testing.ui.espresso.action.AdapterDataLoaderAction;
+import com.google.android.apps.common.testing.ui.espresso.action.AdapterViewProtocol;
+import com.google.android.apps.common.testing.ui.espresso.action.AdapterViewProtocol.AdaptedData;
+import com.google.android.apps.common.testing.ui.espresso.action.AdapterViewProtocols;
+import com.google.android.apps.common.testing.ui.espresso.matcher.RootMatchers;
+import com.google.common.base.Optional;
+
+import android.view.View;
+import android.view.ViewParent;
+import android.widget.Adapter;
+import android.widget.AdapterView;
+
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+
+/**
+ * An interface to interact with data displayed in AdapterViews.
+ * <p>
+ * This interface builds on top of {@link ViewInteraction} and should be the preferred way to
+ * interact with elements displayed inside AdapterViews.
+ * </p>
+ * <p>
+ * This is necessary because an AdapterView may not load all the data held by its Adapter into the
+ * view hierarchy until a user interaction makes it necessary. Also it is more fluent / less brittle
+ * to match upon the data object being rendered into the display then the rendering itself.
+ * </p>
+ * <p>
+ * By default, a DataInteraction takes place against any AdapterView found within the current
+ * screen, if you have multiple AdapterView objects displayed, you will need to narrow the selection
+ * by using the inAdapterView method.
+ * </p>
+ * <p>
+ * The check and perform method operate on the top level child of the adapter view, if you need to
+ * operate on a subview (eg: a Button within the list) use the onChildView method before calling
+ * perform or check.
+ * </p>
+ *
+ */
+public class DataInteraction {
+
+  private final Matcher<Object> dataMatcher;
+  private Matcher<View> adapterMatcher = isAssignableFrom(AdapterView.class);
+  private Optional<Matcher<View>> childViewMatcher = Optional.absent();
+  private Optional<Integer> atPosition = Optional.absent();
+  private AdapterViewProtocol adapterViewProtocol = AdapterViewProtocols.standardProtocol();
+  private Matcher<Root> rootMatcher = RootMatchers.DEFAULT;
+
+  DataInteraction(Matcher<Object> dataMatcher) {
+    this.dataMatcher = checkNotNull(dataMatcher);
+  }
+
+  /**
+   * Causes perform and check methods to take place on a specific child view of the view returned
+   * by Adapter.getView()
+   */
+  public DataInteraction onChildView(Matcher<View> childMatcher) {
+    this.childViewMatcher = Optional.of(checkNotNull(childMatcher));
+    return this;
+  }
+
+  /**
+   * Causes this data interaction to work within the Root specified by the given root matcher.
+   */
+  public DataInteraction inRoot(Matcher<Root> rootMatcher) {
+    this.rootMatcher = checkNotNull(rootMatcher);
+    return this;
+  }
+
+  /**
+   * Selects a particular adapter view to operate on, by default we operate on any adapter view
+   * on the screen.
+   */
+  public DataInteraction inAdapterView(Matcher<View> adapterMatcher) {
+    this.adapterMatcher = checkNotNull(adapterMatcher);
+    return this;
+  }
+
+  /**
+   * Selects the view which matches the nth position on the adapter
+   * based on the data matcher.
+   */
+  public DataInteraction atPosition(Integer atPosition) {
+    this.atPosition = Optional.of(checkNotNull(atPosition));
+    return this;
+  }
+
+  /**
+   * Use a different AdapterViewProtocol if the Adapter implementation does not
+   * satisfy the AdapterView contract like (@code ExpandableListView)
+   */
+  public DataInteraction usingAdapterViewProtocol(AdapterViewProtocol adapterViewProtocol) {
+    this.adapterViewProtocol = checkNotNull(adapterViewProtocol);
+    return this;
+  }
+
+  /**
+   * Performs an action on the view after we force the data to be loaded.
+   *
+   * @return an {@link ViewInteraction} for more assertions or actions.
+   */
+  public ViewInteraction perform(ViewAction... actions) {
+     AdapterDataLoaderAction adapterDataLoaderAction = load();
+
+    return onView(makeTargetMatcher(adapterDataLoaderAction))
+        .inRoot(rootMatcher)
+        .perform(actions);
+  }
+
+  /**
+   * Performs an assertion on the state of the view after we force the data to be loaded.
+   *
+   * @return an {@link ViewInteraction} for more assertions or actions.
+   */
+  public ViewInteraction check(ViewAssertion assertion) {
+     AdapterDataLoaderAction adapterDataLoaderAction = load();
+
+    return onView(makeTargetMatcher(adapterDataLoaderAction))
+        .inRoot(rootMatcher)
+        .check(assertion);
+  }
+
+  private AdapterDataLoaderAction load() {
+    AdapterDataLoaderAction adapterDataLoaderAction =
+       new AdapterDataLoaderAction(dataMatcher, atPosition, adapterViewProtocol);
+    onView(adapterMatcher)
+      .inRoot(rootMatcher)
+      .perform(adapterDataLoaderAction);
+    return adapterDataLoaderAction;
+  }
+
+  @SuppressWarnings("unchecked")
+  private Matcher<View> makeTargetMatcher(AdapterDataLoaderAction adapterDataLoaderAction) {
+    Matcher<View> targetView = displayingData(adapterMatcher, dataMatcher, adapterViewProtocol,
+        adapterDataLoaderAction);
+    if (childViewMatcher.isPresent()) {
+      targetView = allOf(childViewMatcher.get(), isDescendantOfA(targetView));
+    }
+    return targetView;
+  }
+
+  private Matcher<View> displayingData(
+      final Matcher<View> adapterMatcher,
+      final Matcher<Object> dataMatcher,
+      final AdapterViewProtocol adapterViewProtocol,
+      final AdapterDataLoaderAction adapterDataLoaderAction) {
+    checkNotNull(adapterMatcher);
+    checkNotNull(dataMatcher);
+    checkNotNull(adapterViewProtocol);
+
+    return new TypeSafeMatcher<View>() {
+      @Override
+      public void describeTo(Description description) {
+        description.appendText(" displaying data matching: ");
+        dataMatcher.describeTo(description);
+        description.appendText(" within adapter view matching: ");
+        adapterMatcher.describeTo(description);
+      }
+
+      @SuppressWarnings("unchecked")
+      @Override
+      public boolean matchesSafely(View view) {
+
+        ViewParent parent = view.getParent();
+
+        while (parent != null && !(parent instanceof AdapterView)) {
+          parent = parent.getParent();
+        }
+
+        if (parent != null && adapterMatcher.matches(parent)) {
+          Optional<AdaptedData> data = adapterViewProtocol.getDataRenderedByView(
+              (AdapterView<? extends Adapter>) parent, view);
+          if (data.isPresent()) {
+            return adapterDataLoaderAction.getAdaptedData().opaqueToken.equals(
+                data.get().opaqueToken);
+          }
+        }
+        return false;
+      }
+    };
+  }
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/Espresso.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/Espresso.java
new file mode 100644
index 0000000..5e3d5f4
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/Espresso.java
@@ -0,0 +1,255 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso;
+
+import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click;
+import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.pressMenuKey;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isDisplayed;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isRoot;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withClassName;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withContentDescription;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.anyOf;
+import static org.hamcrest.Matchers.endsWith;
+
+import com.google.android.apps.common.testing.ui.espresso.action.ViewActions;
+import com.google.android.apps.common.testing.ui.espresso.base.BaseLayerModule;
+import com.google.android.apps.common.testing.ui.espresso.base.IdlingResourceRegistry;
+import com.google.android.apps.common.testing.ui.espresso.util.TreeIterables;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.Looper;
+import android.view.View;
+import android.view.ViewConfiguration;
+
+import dagger.ObjectGraph;
+
+import org.hamcrest.Matcher;
+
+/**
+ * Entry point to the Espresso framework. Test authors can initiate testing by using one of the on*
+ * methods (e.g. onView) or perform top-level user actions (e.g. pressBack).
+ */
+public final class Espresso {
+
+  static ObjectGraph espressoGraph() {
+    return GraphHolder.graph();
+  }
+
+  private Espresso() {}
+
+  /**
+   * Creates an {@link PartiallyScopedViewInteraction} for a given view. Note: the view has
+   * to be part of the  view hierarchy. This may not be the case if it is rendered as part of
+   * an AdapterView (e.g. ListView). If this is the case, use Espresso.onData to load the view
+   * first.
+   *
+   * @param viewMatcher used to select the view.
+   * @see #onData
+   */
+  public static ViewInteraction onView(final Matcher<View> viewMatcher) {
+    return espressoGraph().plus(new ViewInteractionModule(viewMatcher)).get(ViewInteraction.class);
+  }
+
+
+
+  /**
+   * Creates an {@link DataInteraction} for a data object displayed by the application. Use this
+   * method to load (into the view hierarchy) items from AdapterView widgets (e.g. ListView).
+   *
+   * @param dataMatcher a matcher used to find the data object.
+   */
+  public static DataInteraction onData(Matcher<Object> dataMatcher) {
+    return new DataInteraction(dataMatcher);
+  }
+
+  /**
+   * Registers a Looper for idle checking with the framework. This is intended for use with
+   * non-UI thread loopers.
+   *
+   * @throws IllegalArgumentException if looper is the main looper.
+   */
+  public static void registerLooperAsIdlingResource(Looper looper) {
+    registerLooperAsIdlingResource(looper, false);
+  }
+
+  /**
+   * Registers a Looper for idle checking with the framework. This is intended for use with
+   * non-UI thread loopers.
+   *
+   * This method allows the caller to consider Thread.State.WAIT to be 'idle'.
+   *
+   * This is useful in the case where a looper is sending a message to the UI thread synchronously
+   * through a wait/notify mechanism.
+   *
+   * @throws IllegalArgumentException if looper is the main looper.
+   */
+  public static void registerLooperAsIdlingResource(Looper looper, boolean considerWaitIdle) {
+    espressoGraph().get(IdlingResourceRegistry.class).registerLooper(looper, considerWaitIdle);
+  }
+
+  /**
+   * Registers one or more {@link IdlingResource}s with the framework. It is expected, although not
+   * strictly required, that this method will be called at test setup time prior to any interaction
+   * with the application under test. When registering more than one resource, ensure that each has
+   * a unique name.
+   */
+  public static void registerIdlingResources(IdlingResource... resources) {
+    checkNotNull(resources);
+    IdlingResourceRegistry registry = espressoGraph().get(IdlingResourceRegistry.class);
+    for (IdlingResource resource : resources) {
+      checkNotNull(resource.getName(), "IdlingResource.getName() should not be null");
+      registry.register(resource);
+    }
+  }
+
+  /**
+   * Changes the default {@link FailureHandler} to the given one.
+   */
+  public static void setFailureHandler(FailureHandler failureHandler) {
+    espressoGraph().get(BaseLayerModule.FailureHandlerHolder.class)
+        .update(checkNotNull(failureHandler));
+  }
+
+  /********************************** Top Level Actions ******************************************/
+
+  // Ideally, this should be only allOf(isDisplayed(), withContentDescription("More options"))
+  // But the ActionBarActivity compat lib is missing a content description for this element, so
+  // we add the class name matcher as another option to find the view.
+  @SuppressWarnings("unchecked")
+  private static final Matcher<View> OVERFLOW_BUTTON_MATCHER = anyOf(
+    allOf(isDisplayed(), withContentDescription("More options")), 
+    allOf(isDisplayed(), withClassName(endsWith("OverflowMenuButton"))));
+
+
+  /**
+   * Closes soft keyboard if open.
+   */
+  public static void closeSoftKeyboard() {
+    onView(isRoot()).perform(ViewActions.closeSoftKeyboard());
+  }
+
+  /**
+   * Opens the overflow menu displayed in the contextual options of an ActionMode.
+   *
+   * This works with both native and SherlockActionBar action modes.
+   *
+   * Note the significant difference in UX between ActionMode and ActionBar overflows - ActionMode
+   * will always present an overflow icon and that icon only responds to clicks. The menu button
+   * (if present) has no impact on it.
+   */
+  @SuppressWarnings("unchecked")
+  public static void openContextualActionModeOverflowMenu() {
+    onView(isRoot())
+        .perform(new TransitionBridgingViewAction());
+
+    onView(OVERFLOW_BUTTON_MATCHER)
+        .perform(click());
+  }
+
+  /**
+   * Press on the back button.
+   *
+   * @throws PerformException if currently displayed activity is root activity, since pressing back
+   *         button would result in application closing.
+   */
+  public static void pressBack() {
+    onView(isRoot()).perform(ViewActions.pressBack());
+  }
+
+  /**
+   * Opens the overflow menu displayed within an ActionBar.
+   *
+   * This works with both native and SherlockActionBar ActionBars.
+   *
+   * Note the significant differences of UX between ActionMode and ActionBars with respect to
+   * overflows. If a hardware menu key is present, the overflow icon is never displayed in
+   * ActionBars and can only be interacted with via menu key presses.
+   */
+  @SuppressWarnings("unchecked")
+  public static void openActionBarOverflowOrOptionsMenu(Context context) {
+    if (context.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.HONEYCOMB) {
+      // regardless of the os level of the device, this app will be rendering a menukey
+      // in the virtual navigation bar (if present) or responding to hardware option keys on
+      // any activity.
+      onView(isRoot())
+          .perform(pressMenuKey());
+    } else if (hasVirtualOverflowButton(context)) {
+      // If we're using virtual keys - theres a chance we're in mid animation of switching
+      // between a contextual action bar and the non-contextual action bar. In this case there
+      // are 2 'More Options' buttons present. Lets wait till that is no longer the case.
+      onView(isRoot())
+          .perform(new TransitionBridgingViewAction());
+
+      onView(OVERFLOW_BUTTON_MATCHER)
+          .perform(click());
+    } else {
+      // either a hardware button exists, or we're on a pre-HC os.
+      onView(isRoot())
+          .perform(pressMenuKey());
+    }
+  }
+
+  private static boolean hasVirtualOverflowButton(Context context) {
+    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+      return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB;
+    } else {
+      return !ViewConfiguration.get(context).hasPermanentMenuKey();
+    }
+  }
+
+  /**
+   * Handles the cases where the app is transitioning between a contextual action bar and a
+   * non contextual action bar.
+   */
+  private static class TransitionBridgingViewAction implements ViewAction {
+    @Override
+    public void perform(UiController controller, View view) {
+      int loops = 0;
+      while (isTransitioningBetweenActionBars(view) && loops < 100) {
+        loops++;
+        controller.loopMainThreadForAtLeast(50);
+      }
+      // if we're not transitioning properly the next viewaction
+      // will give a decent enough exception.
+    }
+
+    @Override
+    public String getDescription() {
+      return "Handle transition between action bar and action bar context.";
+    }
+
+    @Override
+    public Matcher<View> getConstraints() {
+      return isRoot();
+    }
+
+    private boolean isTransitioningBetweenActionBars(View view) {
+      int actionButtonCount = 0;
+      for (View child : TreeIterables.breadthFirstViewTraversal(view)) {
+        if (OVERFLOW_BUTTON_MATCHER.matches(child)) {
+          actionButtonCount++;
+        }
+      }
+      return actionButtonCount > 1;
+    }
+  }
+
+
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/EspressoException.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/EspressoException.java
new file mode 100644
index 0000000..4c6a5a2
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/EspressoException.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso;
+
+/**
+ * Used for identifying an exception as coming from the {@link Espresso} framework.
+ */
+public interface EspressoException {}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/FailureHandler.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/FailureHandler.java
new file mode 100644
index 0000000..e0fb9c0
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/FailureHandler.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso;
+
+import android.view.View;
+
+import org.hamcrest.Matcher;
+
+
+
+/**
+ * Handles failures that happen during test execution.
+ */
+public interface FailureHandler {
+
+  /**
+   * Handle the given error in a manner that makes sense to the environment in which the test is
+   * executed (e.g. take a screenshot, output extra debug info, etc). Upon handling, most handlers
+   * will choose to propagate the error.
+   */
+  public void handle(Throwable error, Matcher<View> viewMatcher);
+
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/GraphHolder.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/GraphHolder.java
new file mode 100644
index 0000000..3ee8e55
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/GraphHolder.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.android.apps.common.testing.testrunner.UsageTrackerRegistry;
+import com.google.android.apps.common.testing.ui.espresso.base.BaseLayerModule;
+import com.google.android.apps.common.testing.ui.espresso.base.IdlingResourceRegistry;
+
+import dagger.Module;
+import dagger.ObjectGraph;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Holds Espresso's ObjectGraph.
+ */
+public final class GraphHolder {
+
+  private static final AtomicReference<GraphHolder> instance =
+      new AtomicReference<GraphHolder>(null);
+
+  private final ObjectGraph graph;
+
+  private GraphHolder(ObjectGraph graph) {
+    this.graph = checkNotNull(graph);
+  }
+
+  static ObjectGraph graph() {
+    GraphHolder instanceRef = instance.get();
+    if (null == instanceRef) {
+      instanceRef = new GraphHolder(ObjectGraph.create(EspressoModule.class));
+      if (instance.compareAndSet(null, instanceRef)) {
+        UsageTrackerRegistry.getInstance().trackUsage("Espresso");
+        return instanceRef.graph;
+      } else {
+        return instance.get().graph;
+      }
+    } else {
+      return instanceRef.graph;
+    }
+  }
+
+  // moe:begin_strip
+  public static void initialize(Object... modules) {
+    checkNotNull(modules);
+    Object[] allModules = new Object[modules.length + 1];
+    allModules[0] = EspressoModule.class;
+    System.arraycopy(modules, 0, allModules, 1, modules.length);
+    GraphHolder holder = new GraphHolder(ObjectGraph.create(modules));
+    checkState(instance.compareAndSet(null, holder), "Espresso already initialized.");
+  }
+  // moe:end_strip
+
+  @Module(
+    includes = BaseLayerModule.class,
+    injects = IdlingResourceRegistry.class
+  )
+  static class EspressoModule {
+  }
+
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/IdlingPolicies.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/IdlingPolicies.java
new file mode 100644
index 0000000..17fdc8d
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/IdlingPolicies.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Allows users fine grain control over idling policies.
+ *
+ * Espresso's default idling policies are suitable for most usecases - however
+ * certain execution environments (like the ARM emulator) might be very slow.
+ * This class allows users the ability to adjust defaults to sensible values
+ * for their environments.
+ */
+public final class IdlingPolicies {
+
+  private IdlingPolicies() { }
+
+  private static volatile IdlingPolicy masterIdlingPolicy = new IdlingPolicy.Builder()
+      .withIdlingTimeout(60)
+      .withIdlingTimeoutUnit(TimeUnit.SECONDS)
+      .throwAppNotIdleException()
+      .build();
+
+
+  private static volatile IdlingPolicy dynamicIdlingResourceErrorPolicy = new IdlingPolicy.Builder()
+      .withIdlingTimeout(26)
+      .withIdlingTimeoutUnit(TimeUnit.SECONDS)
+      .throwIdlingResourceTimeoutException()
+      .build();
+
+  private static volatile IdlingPolicy dynamicIdlingResourceWarningPolicy =
+      new IdlingPolicy.Builder()
+        .withIdlingTimeout(5)
+        .withIdlingTimeoutUnit(TimeUnit.SECONDS)
+        .logWarning()
+        .build();
+
+
+  /**
+   * Updates the IdlingPolicy used in UiController.loopUntil to detect AppNotIdleExceptions.
+   *
+   * @param timeout the timeout before an AppNotIdleException is created.
+   * @param unit the unit of the timeout value.
+   */
+  public static void setMasterPolicyTimeout(long timeout, TimeUnit unit) {
+    checkArgument(timeout > 0);
+    checkNotNull(unit);
+    masterIdlingPolicy = masterIdlingPolicy.toBuilder()
+        .withIdlingTimeout(timeout)
+        .withIdlingTimeoutUnit(unit)
+        .build();
+  }
+
+  /**
+   * Updates the IdlingPolicy used by IdlingResourceRegistry to determine when IdlingResources
+   * timeout.
+   *
+   * @param timeout the timeout before an IdlingResourceTimeoutException is created.
+   * @param unit the unit of the timeout value.
+   */
+  public static void setIdlingResourceTimeout(long timeout, TimeUnit unit) {
+    checkArgument(timeout > 0);
+    checkNotNull(unit);
+    dynamicIdlingResourceErrorPolicy = dynamicIdlingResourceErrorPolicy.toBuilder()
+        .withIdlingTimeout(timeout)
+        .withIdlingTimeoutUnit(unit)
+        .build();
+  }
+
+
+  public static IdlingPolicy getMasterIdlingPolicy() {
+    return masterIdlingPolicy;
+  }
+
+  public static IdlingPolicy getDynamicIdlingResourceWarningPolicy() {
+    return dynamicIdlingResourceWarningPolicy;
+  }
+
+  public static IdlingPolicy getDynamicIdlingResourceErrorPolicy() {
+    return dynamicIdlingResourceErrorPolicy;
+  }
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/IdlingPolicy.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/IdlingPolicy.java
new file mode 100644
index 0000000..533afdd
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/IdlingPolicy.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import android.util.Log;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Allows users to control idling idleTimeouts in Espresso.
+ */
+public final class IdlingPolicy {
+  private static final String TAG = "IdlingPolicy";
+  private enum ResponseAction { THROW_APP_NOT_IDLE, THROW_IDLE_TIMEOUT, LOG_ERROR };
+
+  private final long idleTimeout;
+  private final TimeUnit unit;
+  private final ResponseAction errorHandler;
+
+  /**
+   * The amount of time the policy allows a resource to be non-idle.
+   */
+  public long getIdleTimeout(){
+    return idleTimeout;
+  }
+
+  /**
+   * The unit for {@linkgetIdleTimeout}.
+   */
+  public TimeUnit getIdleTimeoutUnit() {
+    return unit;
+  }
+
+  /**
+   * Invoked when the idle idleTimeout has been exceeded.
+   *
+   * @param busyResources the resources that are not idle.
+   * @param message an additional message to include in an exception.
+   */
+  public void handleTimeout(List<String> busyResources, String message) {
+    switch (errorHandler) {
+      case THROW_APP_NOT_IDLE:
+        throw AppNotIdleException.create(busyResources, message);
+      case THROW_IDLE_TIMEOUT:
+        throw new IdlingResourceTimeoutException(busyResources);
+      case LOG_ERROR:
+        Log.w(TAG, "These resources are not idle: " + busyResources);
+        break;
+      default:
+        throw new IllegalStateException("should never reach here." + busyResources);
+    }
+  }
+
+  Builder toBuilder() {
+    return new Builder(this);
+  }
+
+  private IdlingPolicy(Builder builder) {
+    checkArgument(builder.idleTimeout > 0);
+    this.idleTimeout = builder.idleTimeout;
+    this.unit = checkNotNull(builder.unit);
+    this.errorHandler = checkNotNull(builder.errorHandler);
+  }
+
+  static class Builder {
+    private long idleTimeout = -1;
+    private TimeUnit unit = null;
+    private ResponseAction errorHandler = null;
+
+    public Builder() { }
+
+    public IdlingPolicy build() {
+      return new IdlingPolicy(this);
+    }
+
+    private Builder(IdlingPolicy copy) {
+      this.idleTimeout = copy.idleTimeout;
+      this.unit = copy.unit;
+      this.errorHandler = copy.errorHandler;
+    }
+
+    public Builder withIdlingTimeout(long idleTimeout) {
+      this.idleTimeout = idleTimeout;
+      return this;
+    }
+
+    public Builder withIdlingTimeoutUnit(TimeUnit unit) {
+      this.unit = unit;
+      return this;
+    }
+
+    public Builder throwAppNotIdleException() {
+      this.errorHandler = ResponseAction.THROW_APP_NOT_IDLE;
+      return this;
+    }
+
+    public Builder throwIdlingResourceTimeoutException() {
+      this.errorHandler = ResponseAction.THROW_IDLE_TIMEOUT;
+      return this;
+    }
+
+    public Builder logWarning() {
+      this.errorHandler = ResponseAction.LOG_ERROR;
+      return this;
+    }
+  }
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/IdlingResourceTimeoutException.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/IdlingResourceTimeoutException.java
new file mode 100644
index 0000000..6a9ec69
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/IdlingResourceTimeoutException.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.List;
+
+/**
+ * Indicates that an {@link IdlingResource}, which has been registered with the framework, has not
+ * idled within the allowed time.
+ *
+ * Since it is not safe to proceed with test execution while the registered resource is busy (as it
+ * is likely to cause inconsistent results in the test), this is an unrecoverable error. The test
+ * author should verify that the {@link IdlingResource} interface has been implemented correctly.
+ */
+public final class IdlingResourceTimeoutException extends RuntimeException
+    implements EspressoException {
+
+  public IdlingResourceTimeoutException(List<String> resourceNames) {
+    super(String.format("Wait for %s to become idle timed out", checkNotNull(resourceNames)));
+  }
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/InjectEventSecurityException.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/InjectEventSecurityException.java
new file mode 100644
index 0000000..5e6158e
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/InjectEventSecurityException.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso;
+
+/**
+ * An checked {@link Exception} indicating that event injection failed with a
+ * {@link SecurityException}.
+ */
+public final class InjectEventSecurityException extends Exception implements EspressoException {
+
+  public InjectEventSecurityException(String message) {
+    super(message);
+  }
+
+  public InjectEventSecurityException(Throwable cause) {
+    super(cause);
+  }
+
+  public InjectEventSecurityException(String message, Throwable cause) {
+    super(message, cause);
+  }
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/NoActivityResumedException.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/NoActivityResumedException.java
new file mode 100644
index 0000000..77f1c03
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/NoActivityResumedException.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso;
+
+/**
+ * An exception which indicates that there are no activities in stage RESUMED.
+ */
+public final class NoActivityResumedException extends RuntimeException
+    implements EspressoException {
+  public NoActivityResumedException(String description) {
+    super(description);
+  }
+
+  public NoActivityResumedException(String description, Throwable cause) {
+    super(description, cause);
+  }
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/NoMatchingRootException.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/NoMatchingRootException.java
new file mode 100644
index 0000000..9b02aa6
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/NoMatchingRootException.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import org.hamcrest.Matcher;
+
+import java.util.List;
+
+/**
+ * Indicates that a given matcher did not match any {@link Root}s (windows) from those that are
+ * currently available.
+ */
+public final class NoMatchingRootException extends RuntimeException implements EspressoException {
+
+  private NoMatchingRootException(String description) {
+    super(description);
+  }
+
+  public static NoMatchingRootException create(Matcher<Root> rootMatcher, List<Root> roots) {
+    checkNotNull(rootMatcher);
+    checkNotNull(roots);
+    return new NoMatchingRootException(String.format(
+        "Matcher '%s' did not match any of the following roots: %s", rootMatcher, roots));
+  }
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/NoMatchingViewException.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/NoMatchingViewException.java
new file mode 100644
index 0000000..984f206
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/NoMatchingViewException.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.android.apps.common.testing.ui.espresso.util.HumanReadables;
+import com.google.common.base.Optional;
+import com.google.common.collect.Lists;
+
+import android.view.View;
+
+import org.hamcrest.Matcher;
+
+import java.util.List;
+
+/**
+ * Indicates that a given matcher did not match any elements in the view hierarchy.
+ * <p>
+ * Contains details about the matcher and the current view hierarchy to aid in debugging.
+ * </p>
+ * <p>
+ * Since this is usually an unrecoverable error this exception is a runtime exception.
+ * </p>
+ * <p>
+ * References to the view and failing matcher are purposefully not included in the state of this
+ * object - since it will most likely be created on the UI thread and thrown on the instrumentation
+ * thread, it would be invalid to touch the view on the instrumentation thread. Also the view
+ * hierarchy may have changed since exception creation (leading to more confusion).
+ * </p>
+ */
+public final class NoMatchingViewException extends RuntimeException implements EspressoException {
+
+  private Matcher<? super View> viewMatcher;
+  private View rootView;
+  private List<View> adapterViews = Lists.newArrayList();
+  private boolean includeViewHierarchy = true;
+  private Optional<String> adapterViewWarning = Optional.<String>absent();
+
+  private NoMatchingViewException(String description) {
+    super(description);
+  }
+
+  private NoMatchingViewException(Builder builder) {
+    super(getErrorMessage(builder));
+    this.viewMatcher = builder.viewMatcher;
+    this.rootView = builder.rootView;
+    this.adapterViews = builder.adapterViews;
+    this.adapterViewWarning = builder.adapterViewWarning;
+    this.includeViewHierarchy = builder.includeViewHierarchy;
+  }
+
+  private static String getErrorMessage(Builder builder) {
+    String errorMessage = "";
+    if (builder.includeViewHierarchy) {
+      String message = String.format("No views in hierarchy found matching: %s",
+          builder.viewMatcher);
+      if (builder.adapterViewWarning.isPresent()) {
+        message = message + builder.adapterViewWarning.get();
+      }
+      errorMessage = HumanReadables.getViewHierarchyErrorMessage(builder.rootView,
+          null /* problemViews */,
+          message,
+          null /* problemViewSuffix */);
+    } else {
+      errorMessage = String.format("Could not find a view that matches %s" , builder.viewMatcher);
+    }
+    return errorMessage;
+  }
+
+  /** Builder for {@link NoMatchingViewException}. */
+  public static class Builder {
+
+    private Matcher<? super View> viewMatcher;
+    private View rootView;
+    private List<View> adapterViews = Lists.newArrayList();
+    private boolean includeViewHierarchy = true;
+    private Optional<String> adapterViewWarning = Optional.<String>absent();
+
+    public Builder from(NoMatchingViewException exception) {
+      this.viewMatcher = exception.viewMatcher;
+      this.rootView = exception.rootView;
+      this.adapterViews = exception.adapterViews;
+      this.adapterViewWarning = exception.adapterViewWarning;
+      this.includeViewHierarchy = exception.includeViewHierarchy;
+      return this;
+    }
+
+    public Builder withViewMatcher(Matcher<? super View> viewMatcher) {
+      this.viewMatcher = viewMatcher;
+      return this;
+    }
+
+    public Builder withRootView(View rootView) {
+      this.rootView = rootView;
+      return this;
+    }
+
+    public Builder withAdapterViews(List<View> adapterViews) {
+      this.adapterViews = adapterViews;
+      return this;
+    }
+
+    public Builder includeViewHierarchy(boolean includeViewHierarchy) {
+      this.includeViewHierarchy = includeViewHierarchy;
+      return this;
+    }
+
+    public Builder withAdapterViewWarning(Optional<String> adapterViewWarning) {
+      this.adapterViewWarning = adapterViewWarning;
+      return this;
+    }
+
+    public NoMatchingViewException build() {
+      checkNotNull(viewMatcher);
+      checkNotNull(rootView);
+      checkNotNull(adapterViews);
+      checkNotNull(adapterViewWarning);
+      return new NoMatchingViewException(this);
+    }
+  }
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/PerformException.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/PerformException.java
new file mode 100644
index 0000000..ac18e77
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/PerformException.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Indicates that an exception occurred while performing a ViewAction on the UI thread.
+ *
+ * A description of the {@link ViewAction}, the view being performed on and the cause are included
+ * in the error. Note: {@link FailureHandler}s can mutate the exception later to make it more user
+ * friendly.
+ *
+ * This is generally not recoverable so it is thrown on the instrumentation thread.
+ */
+public final class PerformException extends RuntimeException implements EspressoException {
+
+  private static final String MESSAGE_FORMAT = "Error performing '%s' on view '%s'.";
+
+  private final String actionDescription;
+  private final String viewDescription;
+
+  private PerformException(Builder builder) {
+    super(String.format(MESSAGE_FORMAT, builder.actionDescription, builder.viewDescription),
+        builder.cause);
+    this.actionDescription = checkNotNull(builder.actionDescription);
+    this.viewDescription = checkNotNull(builder.viewDescription);
+  }
+
+  public String getActionDescription() {
+    return actionDescription;
+  }
+
+  public String getViewDescription() {
+    return viewDescription;
+  }
+
+  /**
+   * Builder for {@link PerformException}.
+   */
+  public static class Builder {
+    private String actionDescription;
+    private String viewDescription;
+    private Throwable cause;
+
+    public Builder from(PerformException instance) {
+      this.actionDescription = instance.getActionDescription();
+      this.viewDescription = instance.getViewDescription();
+      this.cause = instance.getCause();
+      return this;
+    }
+
+    public Builder withActionDescription(String actionDescription) {
+      this.actionDescription = actionDescription;
+      return this;
+    }
+
+    public Builder withViewDescription(String viewDescription) {
+      this.viewDescription = viewDescription;
+      return this;
+    }
+
+    public Builder withCause(Throwable cause) {
+      this.cause = cause;
+      return this;
+    }
+
+    public PerformException build() {
+      return new PerformException(this);
+    }
+  }
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/Root.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/Root.java
new file mode 100644
index 0000000..0d900de
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/Root.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso;
+
+import static com.google.common.base.Objects.toStringHelper;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.android.apps.common.testing.ui.espresso.util.HumanReadables;
+import com.google.common.base.Objects.ToStringHelper;
+import com.google.common.base.Optional;
+
+import android.view.View;
+import android.view.WindowManager;
+
+/**
+ * Represents a root view in the application and optionally the layout params of the window holding
+ * it.
+ *
+ * This class is used internally to determine which view root to run user provided matchers against
+ * it is not part of the public api.
+ */
+public final class Root {
+  private final View decorView;
+  private final Optional<WindowManager.LayoutParams> windowLayoutParams;
+
+  private Root(Builder builder) {
+    this.decorView = checkNotNull(builder.decorView);
+    this.windowLayoutParams = Optional.fromNullable(builder.windowLayoutParams);
+  }
+
+  public View getDecorView() {
+    return decorView;
+  }
+
+  public Optional<WindowManager.LayoutParams> getWindowLayoutParams() {
+    return windowLayoutParams;
+  }
+
+  @Override
+  public String toString() {
+    ToStringHelper helper = toStringHelper(this)
+        .add("application-window-token", decorView.getApplicationWindowToken())
+        .add("window-token", decorView.getWindowToken())
+        .add("has-window-focus", decorView.hasWindowFocus());
+    if (windowLayoutParams.isPresent()) {
+      helper
+          .add("layout-params-type", windowLayoutParams.get().type)
+          .add("layout-params-string", windowLayoutParams.get());
+    }
+    helper
+        .add("decor-view-string", HumanReadables.describe(decorView));
+    return helper.toString();
+  }
+
+  public static class Builder {
+    private View decorView;
+    private WindowManager.LayoutParams windowLayoutParams;
+
+    public Root build() {
+      return new Root(this);
+    }
+
+    public Builder withDecorView(View view) {
+      this.decorView = view;
+      return this;
+    }
+
+    public Builder withWindowLayoutParams(WindowManager.LayoutParams windowLayoutParams) {
+      this.windowLayoutParams = windowLayoutParams;
+      return this;
+    }
+  }
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/UiController.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/UiController.java
new file mode 100644
index 0000000..cf53d08
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/UiController.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso;
+
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+/**
+ * Provides base-level UI operations (such as injection of {@link MotionEvent}s) that can be used to
+ * build user actions such as clicks, scrolls, swipes, etc. This replaces parts of the android
+ * Instrumentation class that provides similar functionality. However, it provides a more advanced
+ * synchronization mechanism for test actions. The key differentiators are:
+ * <ul>
+ * <li>test actions are assumed to be called on the main thread
+ * <li>after a test action is initiated, execution blocks until all messages in the main message
+ * queue have been cleared.
+ * </ul>
+ */
+public interface UiController {
+  /**
+   * Injects a motion event into the application.
+   *
+   * @param event the (non-null!) event to inject
+   * @return true if the event was injected, false otherwise
+   * @throws InjectEventSecurityException if the event couldn't be injected because it would
+   *         interact with another application.
+   */
+  boolean injectMotionEvent(MotionEvent event) throws InjectEventSecurityException;
+
+  /**
+   * Injects a key event into the application.
+   *
+   * @param event the (non-null!) event to inject
+   * @return true if the event was injected, false otherwise
+   * @throws InjectEventSecurityException if the event couldn't be injected because it would
+   *         interact with another application.
+   */
+  boolean injectKeyEvent(KeyEvent event) throws InjectEventSecurityException;
+
+  /**
+   * Types a string into the application using series of {@link KeyEvent}s. It is up to the
+   * implementor to decide how to map the string to {@link KeyEvent} objects. if you need specific
+   * control over the key events generated use {@link #injectKeyEvent(KeyEvent)}.
+   *
+   * @param str the (non-null!) string to type
+   * @return true if the string was injected, false otherwise
+   * @throws InjectEventSecurityException if the events couldn't be injected because it would
+   *         interact with another application.
+   */
+  boolean injectString(String str) throws InjectEventSecurityException;
+
+  /**
+   * Loops the main thread until the application goes idle.
+   *
+   *  An empty task is immediately inserted into the task queue to ensure that if we're idle at this
+   * moment we'll return instantly.
+   */
+  void loopMainThreadUntilIdle();
+
+  /**
+   * Loops the main thread for a specified period of time.
+   *
+   *  Control may not return immediately, instead it'll return after the time has passed and the
+   * queue is in an idle state again.
+   *
+   * @param millisDelay time to spend in looping the main thread
+   */
+  void loopMainThreadForAtLeast(long millisDelay);
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/ViewAction.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/ViewAction.java
new file mode 100644
index 0000000..12e607e
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/ViewAction.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso;
+
+import android.view.View;
+
+import org.hamcrest.Matcher;
+
+/**
+ * Responsible for performing an interaction on the given View element.<br>
+ * <p>
+ * This is part of the test framework public API - developers are free to write their own ViewAction
+ * implementations when necessary. When implementing a new ViewAction, follow these rules:
+ * <ul>
+ * <li>Inject motion events or key events via the UiController to simulate user interactions.
+ * <li>Do not mutate the view directly via setter methods and other state changing methods on the
+ * view parameter.
+ * <li>Do not throw AssertionErrors. Assertions belong in ViewAssertion classes.
+ * <li>View action code will executed on the UI thread, therefore you should not block, perform
+ * sleeps, or perform other expensive computations.
+ * <li>The test framework will wait for the UI thread to be idle both before and after perform() is
+ * called. This means that the action is guaranteed to be synchronized with any other view
+ * operations.
+ * <li>Downcasting the View object to an expected subtype is allowed, so long as the object
+ * expresses the subtype matches the constraints as specified in {@code getConstraints}.
+ * </ul>
+ */
+public interface ViewAction {
+
+  /**
+   * A mechanism for ViewActions to specify what type of views they can operate on.
+   *
+   *  A ViewAction can demand that the view passed to perform meets certain constraints. For example
+   * it may want to ensure the view is already in the viewable physical screen of the device or is
+   * of a certain type.
+   *
+   * @return a {@link Matcher} that will be tested prior to calling perform.
+   */
+  public Matcher<View> getConstraints();
+
+  /**
+   * Returns a description of the view action. The description should not be overly long and should
+   * fit nicely in a sentence like: "performing %description% action on view with id ..."
+   */
+  public String getDescription();
+
+  /**
+   * Performs this action on the given view.
+   *
+   * @param uiController the controller to use to interact with the UI.
+   * @param view the view to act upon. never null.
+   */
+  public void perform(UiController uiController, View view);
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/ViewAssertion.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/ViewAssertion.java
new file mode 100644
index 0000000..9329b57
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/ViewAssertion.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso;
+
+import android.view.View;
+
+import javax.annotation.Nullable;
+
+/**
+ * Responsible for performing assertions on a View element.<br>
+ * <p>
+ * This is considered part of the test framework public API - developers are free to write their own
+ * assertions as long as they meet the following requirements:
+ * <ul>
+ * <li>Do not mutate the passed in view.
+ * <li>Throw junit.framework.AssertionError when the view assertion does not hold.
+ * <li>Implementation runs on the UI thread - so it should not do any blocking operations
+ * <li>Downcasting the view to a specific type is allowed, provided there is a test that view is an
+ * instance of that type before downcasting. If not, an AssertionError should be thrown.
+ * <li>It is encouraged to access non-mutating methods on the view to perform assertion.
+ * </ul>
+ * <br>
+ * <p>
+ * Strongly consider using a existing ViewAssertion via the ViewAssertions utility class before
+ * writing your own assertion.
+ */
+public interface ViewAssertion {
+
+  /**
+   * Checks the state of the given view (if such a view is present).
+   *
+   * @param view the view, if one was found during the view interaction or null if it was not
+   * (which may be an acceptable option for an assertion)
+   * @param noViewFoundException an exception detailing why the view could not be found or null if
+   * the view was found
+   */
+  void check(@Nullable View view, @Nullable NoMatchingViewException noViewFoundException);
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/ViewFinder.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/ViewFinder.java
new file mode 100644
index 0000000..31e0d11
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/ViewFinder.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso;
+
+import android.view.View;
+
+/**
+ * Uses matchers to locate particular views within the view hierarchy.
+ */
+public interface ViewFinder {
+
+  /**
+   * Immediately locates a single view within the provided view hierarchy.
+   *
+   * If multiple views match, or if no views match the appropriate exception is thrown.
+   *
+   * @return A singular view which matches the matcher we were constructed with.
+   * @throws AmbiguousViewMatcherException when multiple views match
+   * @throws NoMatchingViewException when no views match.
+   */
+  public View getView() throws AmbiguousViewMatcherException, NoMatchingViewException;
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/ViewInteraction.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/ViewInteraction.java
new file mode 100644
index 0000000..5207083
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/ViewInteraction.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso;
+
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isAssignableFrom;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isDescendantOfA;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.android.apps.common.testing.ui.espresso.action.ScrollToAction;
+import com.google.android.apps.common.testing.ui.espresso.base.MainThread;
+import com.google.android.apps.common.testing.ui.espresso.util.HumanReadables;
+
+import android.util.Log;
+import android.view.View;
+import android.widget.AdapterView;
+
+import org.hamcrest.Matcher;
+import org.hamcrest.StringDescription;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.inject.Inject;
+
+/**
+ * Provides the primary interface for test authors to perform actions or asserts on views.
+ * <p>
+ * Each interaction is associated with a view identified by a view matcher. All view actions and
+ * asserts are performed on the UI thread (thus ensuring sequential execution). The same goes for
+ * retrieval of views (this is done to ensure that view state is "fresh" prior to execution of each
+ * operation).
+ * <p>
+ */
+public final class ViewInteraction {
+
+  private static final String TAG = ViewInteraction.class.getSimpleName();
+
+  private final UiController uiController;
+  private final ViewFinder viewFinder;
+  private final Executor mainThreadExecutor;
+  private final FailureHandler failureHandler;
+  private final Matcher<View> viewMatcher;
+  private final AtomicReference<Matcher<Root>> rootMatcherRef;
+
+  @Inject
+  ViewInteraction(
+      UiController uiController,
+      ViewFinder viewFinder,
+      @MainThread Executor mainThreadExecutor,
+      FailureHandler failureHandler,
+      Matcher<View> viewMatcher,
+      AtomicReference<Matcher<Root>> rootMatcherRef) {
+    this.viewFinder = checkNotNull(viewFinder);
+    this.uiController = checkNotNull(uiController);
+    this.failureHandler = checkNotNull(failureHandler);
+    this.mainThreadExecutor = checkNotNull(mainThreadExecutor);
+    this.viewMatcher = checkNotNull(viewMatcher);
+    this.rootMatcherRef = checkNotNull(rootMatcherRef);
+  }
+
+  /**
+   * Performs the given action(s) on the view selected by the current view matcher. If more than one
+   * action is provided, actions are executed in the order provided with precondition checks running
+   * prior to each action.
+   *
+   * @param viewActions one or more actions to execute.
+   * @return this interaction for further perform/verification calls.
+   */
+  public ViewInteraction perform(final ViewAction... viewActions) {
+    checkNotNull(viewActions);
+    for (ViewAction action : viewActions) {
+      doPerform(action);
+    }
+    return this;
+  }
+
+
+  /**
+   * Makes this ViewInteraction scoped to the root selected by the given root matcher.
+   */
+  public ViewInteraction inRoot(Matcher<Root> rootMatcher) {
+    this.rootMatcherRef.set(checkNotNull(rootMatcher));
+    return this;
+  }
+
+  private void doPerform(final ViewAction viewAction) {
+    checkNotNull(viewAction);
+    final Matcher<? extends View> constraints = checkNotNull(viewAction.getConstraints());
+    runSynchronouslyOnUiThread(new Runnable() {
+
+      @Override
+      public void run() {
+        uiController.loopMainThreadUntilIdle();
+        View targetView = viewFinder.getView();
+        Log.i(TAG, String.format(
+            "Performing '%s' action on view %s", viewAction.getDescription(), viewMatcher));
+        if (!constraints.matches(targetView)) {
+          // TODO(valeraz): update this to describeMismatch once hamcrest is updated to new
+          // version in google3 (we are waiting for version 1.4 to avoid issues with generics)
+          StringDescription stringDescription = new StringDescription(new StringBuilder(
+              "Action will not be performed because the target view "
+              + "does not match one or more of the following constraints:\n"));
+          constraints.describeTo(stringDescription);
+          stringDescription.appendText("\nTarget view: ")
+              .appendValue(HumanReadables.describe(targetView));
+
+          if (viewAction instanceof ScrollToAction
+              && isDescendantOfA(isAssignableFrom((AdapterView.class))).matches(targetView)) {
+            stringDescription.appendText(
+                "\nFurther Info: ScrollToAction on a view inside an AdapterView will not work. "
+                + "Use Espresso.onData to load the view.");
+          }
+          throw new PerformException.Builder()
+            .withActionDescription(viewAction.getDescription())
+            .withViewDescription(viewMatcher.toString())
+            .withCause(new RuntimeException(stringDescription.toString()))
+            .build();
+        } else {
+          viewAction.perform(uiController, targetView);
+        }
+      }
+    });
+  }
+
+  /**
+   * Checks the given {@link ViewAssertion} on the the view selected by the current view matcher.
+   *
+   * @param viewAssert the assertion to perform.
+   * @return this interaction for further perform/verification calls.
+   */
+  public ViewInteraction check(final ViewAssertion viewAssert) {
+    checkNotNull(viewAssert);
+    runSynchronouslyOnUiThread(new Runnable() {
+      @Override
+      public void run() {
+        uiController.loopMainThreadUntilIdle();
+
+        View targetView = null;
+        NoMatchingViewException missingViewException = null;
+        try {
+          targetView = viewFinder.getView();
+        } catch (NoMatchingViewException nsve) {
+          missingViewException = nsve;
+        }
+        viewAssert.check(targetView, missingViewException);
+      }
+    });
+    return this;
+  }
+
+  private void runSynchronouslyOnUiThread(Runnable action) {
+    FutureTask<Void> uiTask = new FutureTask<Void>(action, null);
+    mainThreadExecutor.execute(uiTask);
+    try {
+      uiTask.get();
+    } catch (InterruptedException ie) {
+      throw new RuntimeException("Interrupted  running UI task", ie);
+    } catch (ExecutionException ee) {
+      failureHandler.handle(ee.getCause(), viewMatcher);
+    }
+  }
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/ViewInteractionModule.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/ViewInteractionModule.java
new file mode 100644
index 0000000..30eccc9
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/ViewInteractionModule.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.android.apps.common.testing.ui.espresso.base.RootViewPicker;
+import com.google.android.apps.common.testing.ui.espresso.base.ViewFinderImpl;
+import com.google.android.apps.common.testing.ui.espresso.matcher.RootMatchers;
+
+import android.view.View;
+
+import dagger.Module;
+import dagger.Provides;
+
+import org.hamcrest.Matcher;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Adds the user interaction scope to the Espresso graph.
+ */
+@Module(
+    addsTo = GraphHolder.EspressoModule.class,
+    injects = {ViewInteraction.class})
+class ViewInteractionModule {
+
+  private final Matcher<View> viewMatcher;
+  private final AtomicReference<Matcher<Root>> rootMatcher =
+      new AtomicReference<Matcher<Root>>(RootMatchers.DEFAULT);
+
+  ViewInteractionModule(Matcher<View> viewMatcher) {
+    this.viewMatcher = checkNotNull(viewMatcher);
+  }
+
+  @Provides
+  AtomicReference<Matcher<Root>> provideRootMatcher() {
+    return rootMatcher;
+  }
+
+  @Provides
+  Matcher<View> provideViewMatcher() {
+    return viewMatcher;
+  }
+
+  @Provides
+  ViewFinder provideViewFinder(ViewFinderImpl impl) {
+    return impl;
+  }
+
+  @Provides
+  public View provideRootView(RootViewPicker rootViewPicker) {
+    // RootsOracle acts as a provider, but returning Providers is illegal, so delegate.
+    return rootViewPicker.get();
+  }
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/AdapterDataLoaderAction.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/AdapterDataLoaderAction.java
new file mode 100644
index 0000000..d682f6b
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/AdapterDataLoaderAction.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.action;
+
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isAssignableFrom;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isDisplayed;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import static org.hamcrest.Matchers.allOf;
+
+import com.google.android.apps.common.testing.ui.espresso.PerformException;
+import com.google.android.apps.common.testing.ui.espresso.UiController;
+import com.google.android.apps.common.testing.ui.espresso.ViewAction;
+import com.google.android.apps.common.testing.ui.espresso.util.HumanReadables;
+import com.google.common.base.Optional;
+import com.google.common.collect.Lists;
+
+import android.view.View;
+import android.widget.Adapter;
+import android.widget.AdapterView;
+
+import org.hamcrest.Matcher;
+import org.hamcrest.StringDescription;
+
+import java.util.List;
+
+/**
+ * Forces an AdapterView to ensure that the data matching a provided data matcher
+ * is loaded into the current view hierarchy.
+ *
+ */
+public final class AdapterDataLoaderAction implements ViewAction {
+  private final Matcher<Object> dataToLoadMatcher;
+  private final AdapterViewProtocol adapterViewProtocol;
+  private final Optional<Integer> atPosition;
+  private AdapterViewProtocol.AdaptedData adaptedData;
+  private boolean performed = false;
+  private Object dataLock = new Object();
+
+  public AdapterDataLoaderAction(Matcher<Object> dataToLoadMatcher, Optional<Integer> atPosition,
+      AdapterViewProtocol adapterViewProtocol) {
+    this.dataToLoadMatcher = checkNotNull(dataToLoadMatcher);
+    this.atPosition = checkNotNull(atPosition);
+    this.adapterViewProtocol = checkNotNull(adapterViewProtocol);
+  }
+
+  public AdapterViewProtocol.AdaptedData getAdaptedData() {
+    synchronized (dataLock) {
+      checkState(performed, "perform hasn't been called yet!");
+      return adaptedData;
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public Matcher<View> getConstraints() {
+    return allOf(isAssignableFrom(AdapterView.class), isDisplayed());
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public void perform(UiController uiController, View view) {
+    AdapterView<? extends Adapter> adapterView = (AdapterView<? extends Adapter>) view;
+    List<AdapterViewProtocol.AdaptedData> matchedDataItems = Lists.newArrayList();
+
+    for (AdapterViewProtocol.AdaptedData data : adapterViewProtocol.getDataInAdapterView(
+        adapterView)) {
+
+      if (dataToLoadMatcher.matches(data.data)) {
+        matchedDataItems.add(data);
+      }
+    }
+
+    if (matchedDataItems.size() == 0) {
+      StringDescription dataMatcherDescription = new StringDescription();
+      dataToLoadMatcher.describeTo(dataMatcherDescription);
+
+      if (matchedDataItems.isEmpty()) {
+        dataMatcherDescription.appendText(" contained values: ");
+          dataMatcherDescription.appendValue(
+              adapterViewProtocol.getDataInAdapterView(adapterView));
+        throw new PerformException.Builder()
+          .withActionDescription(this.getDescription())
+          .withViewDescription(HumanReadables.describe(view))
+          .withCause(new RuntimeException("No data found matching: " + dataMatcherDescription))
+          .build();
+      }
+    }
+
+    synchronized (dataLock) {
+      checkState(!performed, "perform called 2x!");
+      performed = true;
+      if (atPosition.isPresent()) {
+        int matchedDataItemsSize = matchedDataItems.size() - 1;
+        if (atPosition.get() > matchedDataItemsSize) {
+          throw new PerformException.Builder()
+            .withActionDescription(this.getDescription())
+            .withViewDescription(HumanReadables.describe(view))
+            .withCause(new RuntimeException(String.format(
+                "There are only %d elements that matched but requested %d element.",
+                matchedDataItemsSize, atPosition.get())))
+            .build();
+        } else {
+          adaptedData = matchedDataItems.get(atPosition.get());
+        }
+      } else {
+        if (matchedDataItems.size() != 1) {
+          StringDescription dataMatcherDescription = new StringDescription();
+          dataToLoadMatcher.describeTo(dataMatcherDescription);
+          throw new PerformException.Builder()
+            .withActionDescription(this.getDescription())
+            .withViewDescription(HumanReadables.describe(view))
+            .withCause(new RuntimeException("Multiple data elements " +
+                "matched: " + dataMatcherDescription + ". Elements: " + matchedDataItems))
+            .build();
+        } else {
+          adaptedData = matchedDataItems.get(0);
+        }
+      }
+    }
+
+    int requestCount = 0;
+    while (!adapterViewProtocol.isDataRenderedWithinAdapterView(adapterView, adaptedData)) {
+      if (requestCount > 1) {
+        if ((requestCount % 50) == 0) {
+          // sometimes an adapter view will receive an event that will block its attempts to scroll.
+          adapterView.invalidate();
+          adapterViewProtocol.makeDataRenderedWithinAdapterView(adapterView, adaptedData);
+        }
+      } else {
+        adapterViewProtocol.makeDataRenderedWithinAdapterView(adapterView, adaptedData);
+      }
+      uiController.loopMainThreadForAtLeast(100);
+      requestCount++;
+    }
+  }
+
+  @Override
+  public String getDescription() {
+    return "load adapter data";
+  }
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/AdapterViewProtocol.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/AdapterViewProtocol.java
new file mode 100644
index 0000000..c85a76d
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/AdapterViewProtocol.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.action;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import com.google.common.base.Optional;
+
+import android.view.View;
+import android.widget.Adapter;
+import android.widget.AdapterView;
+
+import javax.annotation.Nullable;
+
+/**
+ * A sadly necessary layer of indirection to interact with AdapterViews.
+ * <p>
+ * Generally any subclass should respect the contracts and behaviors of its superclass. Otherwise
+ * it becomes impossible to work generically with objects that all claim to share a supertype - you
+ * need special cases to perform the same operation 'owned' by the supertype for each sub-type. The
+ * 'is - a' relationship is broken.
+ * </p>
+ *
+ * <p>
+ * Android breaks the Liskov substitution principal with ExpandableListView - you can't use
+ * getAdapter(), getItemAtPosition(), and other methods common to AdapterViews on an
+ * ExpandableListView because an ExpandableListView isn't an adapterView - they just share a lot of
+ * code.
+ * </p>
+ *
+ * <p>
+ * This interface exists to work around this wart (which sadly is copied in other projects too) and
+ * lets the implementor translate Espresso's needs and manipulations of the AdapterView into calls
+ * that make sense for the given subtype and context.
+ * </p>
+ *
+ * <p><i>
+ * If you have to implement this to talk to widgets your own project defines - I'm sorry.
+ * </i><p>
+ *
+ */
+public interface AdapterViewProtocol {
+
+  /**
+   * Returns all data this AdapterViewProtocol can find within the given AdapterView.
+   *
+   * <p>
+   * Any AdaptedData returned by this method can be passed to makeDataRenderedWithinView and the
+   * implementation should make the AdapterView bring that data item onto the screen.
+   * </p>
+   *
+   * @param adapterView the AdapterView we want to interrogate the contents of.
+   * @return an {@link Iterable} of AdaptedDatas representing all data the implementation sees in
+   *         this view
+   * @throws IllegalArgumentException if the implementation doesn't know how to manipulate the given
+   *         adapter view.
+   */
+  Iterable<AdaptedData> getDataInAdapterView(AdapterView<? extends Adapter> adapterView);
+
+  /**
+   * Returns the data object this particular view is rendering if possible.
+   *
+   * <p>
+   * Implementations are expected to create a relationship between the data in the AdapterView and
+   * the descendant views of the AdapterView that obeys the following conditions:
+   * </p>
+   *
+   * <ul>
+   * <li>For each descendant view there exists either 0 or 1 data objects it is rendering.</li>
+   * <li>For each data object the AdapterView there exists either 0 or 1 descendant views which
+   *   claim to be rendering it.</li>
+   * </ul>
+   *
+   * <p> For example - if a PersonObject is rendered into: </p>
+   * <code>
+   * LinearLayout
+   *   ImageView picture
+   *   TextView firstName
+   *   TextView lastName
+   * </code>
+   *
+   * <p>
+   * It would be expected that getDataRenderedByView(adapter, LinearLayout) would return the
+   * PersonObject. If it were called instead with the TextView or ImageView it would return
+   * Object.absent().
+   * </p>
+   *
+   * @param adapterView the adapterview hosting the data.
+   * @param descendantView a view which is a child, grand-child, or deeper descendant of adapterView
+   * @return an optional data object the descendant view is rendering.
+   * @throws IllegalArgumentException if this protocol cannot interrogate this class of adapterView
+   */
+  Optional<AdaptedData> getDataRenderedByView(
+      AdapterView<? extends Adapter> adapterView, View descendantView);
+
+  /**
+   * Requests that a particular piece of data held in this AdapterView is actually rendered by it.
+   *
+   * <p>
+   * After calling this method it expected that there will exist some descendant view of adapterView
+   * for which calling getDataRenderedByView(adapterView, descView).get() == data.data is true.
+   * <p>
+   *
+   * </p>
+   * Note: this need not happen immediately. EG: an implementor handling ListView may call
+   * listView.smoothScrollToPosition(data.opaqueToken) - which kicks off an animated scroll over
+   * the list to the given position. The animation may be in progress after this call returns. The
+   * only guarantee is that eventually - with no further interaction necessary - this data item
+   * will be rendered as a child or deeper descendant of this AdapterView.
+   * </p>
+   *
+   * @param adapterView the adapterView hosting the data.
+   * @param data an AdaptedData instance retrieved by a prior call to getDataInAdapterView
+   * @throws IllegalArgumentException if this protocol cannot manipulate adapterView or if data is
+   *   not owned by this AdapterViewProtocol.
+   */
+  void makeDataRenderedWithinAdapterView(
+      AdapterView<? extends Adapter> adapterView, AdaptedData data);
+
+
+  /**
+   * Indicates whether or not there now exists a descendant view within adapterView that
+   * is rendering this data.
+   *
+   * @param adapterView the AdapterView hosting this data.
+   * @param adaptedData the data we are checking the display state for.
+   * @return true if the data is rendered by a view in the adapterView, false otherwise.
+   */
+  boolean isDataRenderedWithinAdapterView(
+      AdapterView<? extends Adapter> adapterView, AdaptedData adaptedData);
+
+
+  /**
+   * A holder that associates a data object from an AdapterView with a token the
+   * AdapterViewProtocol can use to force that data object to be rendered as a child or deeper
+   * descendant of the adapter view.
+   */
+  public static class AdaptedData {
+
+    /**
+     * One of the objects the AdapterView is exposing to the user.
+     */
+    @Nullable
+    public final Object data;
+
+    /**
+     * A token the implementor of AdapterViewProtocol can use to force the adapterView to display
+     * this data object as a child or deeper descendant in it. Equal opaqueToken point to the same
+     * data object on the AdapterView.
+     */
+    public final Object opaqueToken;
+
+    @Override
+    public String toString() {
+      return String.format("Data: %s (class: %s) token: %s", data,
+          null == data ? null : data.getClass(), opaqueToken);
+    }
+
+    private AdaptedData(Object data, Object opaqueToken) {
+      this.data = data;
+      this.opaqueToken = checkNotNull(opaqueToken);
+    }
+
+    public static class Builder {
+      private Object data;
+      private Object opaqueToken;
+
+      public Builder withData(@Nullable Object data) {
+        this.data = data;
+        return this;
+      }
+
+      public Builder withOpaqueToken(@Nullable Object opaqueToken) {
+        this.opaqueToken = opaqueToken;
+        return this;
+      }
+
+      public AdaptedData build() {
+        return new AdaptedData(data, opaqueToken);
+      }
+    }
+  }
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/AdapterViewProtocols.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/AdapterViewProtocols.java
new file mode 100644
index 0000000..5fc6032
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/AdapterViewProtocols.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.action;
+
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isDisplayingAtLeast;
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.base.Optional;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Range;
+
+import android.os.Build;
+import android.view.View;
+import android.widget.AbsListView;
+import android.widget.Adapter;
+import android.widget.AdapterView;
+import android.widget.AdapterViewAnimator;
+import android.widget.AdapterViewFlipper;
+
+import java.util.List;
+
+/**
+ * Implementations of {@link AdapterViewProtocol} for standard SDK Widgets.
+ *
+ */
+public final class AdapterViewProtocols {
+
+  /**
+   * Consider views which have over this percentage of their area visible to the user
+   * to be fully rendered.
+   */
+  private static final int FULLY_RENDERED_PERCENTAGE_CUTOFF = 90;
+
+  private AdapterViewProtocols() {}
+
+  private static final AdapterViewProtocol STANDARD_PROTOCOL = new StandardAdapterViewProtocol();
+
+  /**
+   * Creates an implementation of AdapterViewProtocol that can work with AdapterViews that do not
+   * break method contracts on AdapterView.
+   *
+   */
+  public static AdapterViewProtocol standardProtocol() {
+    return STANDARD_PROTOCOL;
+  }
+
+  // TODO(user): expandablelistview protocols
+
+  private static final class StandardAdapterViewProtocol implements AdapterViewProtocol {
+    @Override
+    public Iterable<AdaptedData> getDataInAdapterView(AdapterView<? extends Adapter> adapterView) {
+      List<AdaptedData> datas = Lists.newArrayList();
+      for (int i = 0; i < adapterView.getCount(); i++) {
+        datas.add(
+            new AdaptedData.Builder()
+              .withData(adapterView.getItemAtPosition(i))
+              .withOpaqueToken(i)
+              .build());
+      }
+      return datas;
+    }
+
+    @Override
+    public Optional<AdaptedData> getDataRenderedByView(AdapterView<? extends Adapter> adapterView,
+        View descendantView) {
+      if (adapterView == descendantView.getParent()) {
+        int position = adapterView.getPositionForView(descendantView);
+        if (position != AdapterView.INVALID_POSITION) {
+          return Optional.of(new AdaptedData.Builder()
+              .withData(adapterView.getItemAtPosition(position))
+              .withOpaqueToken(Integer.valueOf(position))
+              .build());
+        }
+      }
+      return Optional.absent();
+    }
+
+    @Override
+    public void makeDataRenderedWithinAdapterView(
+        AdapterView<? extends Adapter> adapterView, AdaptedData data) {
+      checkArgument(data.opaqueToken instanceof Integer, "Not my data: %s", data);
+      int position = ((Integer) data.opaqueToken).intValue();
+
+      boolean moved = false;
+      // set selection should always work, we can give a little better experience if per subtype
+      // though.
+      if (Build.VERSION.SDK_INT > 7) {
+        if (adapterView instanceof AbsListView) {
+          if (Build.VERSION.SDK_INT > 10) {
+            ((AbsListView) adapterView).smoothScrollToPositionFromTop(position,
+                adapterView.getPaddingTop(), 0);
+          } else {
+            ((AbsListView) adapterView).smoothScrollToPosition(position);
+          }
+          moved = true;
+        }
+        if (Build.VERSION.SDK_INT > 10) {
+          if (adapterView instanceof AdapterViewAnimator) {
+            if (adapterView instanceof AdapterViewFlipper) {
+              ((AdapterViewFlipper) adapterView).stopFlipping();
+            }
+            ((AdapterViewAnimator) adapterView).setDisplayedChild(position);
+            moved = true;
+          }
+        }
+      }
+      if (!moved) {
+        adapterView.setSelection(position);
+      }
+    }
+
+    @SuppressWarnings("deprecation")
+    @Override
+    public boolean isDataRenderedWithinAdapterView(
+        AdapterView<? extends Adapter> adapterView, AdaptedData adaptedData) {
+      checkArgument(adaptedData.opaqueToken instanceof Integer, "Not my data: %s", adaptedData);
+      int dataPosition = ((Integer) adaptedData.opaqueToken).intValue();
+
+      if (Range.closed(adapterView.getFirstVisiblePosition(), adapterView.getLastVisiblePosition())
+          .contains(dataPosition)) {
+        if (adapterView.getFirstVisiblePosition() == adapterView.getLastVisiblePosition()) {
+          // thats a huge element.
+          return true;
+        } else {
+          return isElementFullyRendered(adapterView,
+              dataPosition - adapterView.getFirstVisiblePosition());
+        }
+      } else {
+        return false;
+      }
+    }
+
+    private boolean isElementFullyRendered(AdapterView<? extends Adapter> adapterView,
+        int childAt) {
+      View element = adapterView.getChildAt(childAt);
+      // Occassionally we'll have to fight with smooth scrolling logic on our definition of when
+      // there is extra scrolling to be done. In particular if the element is the first or last
+      // element of the list, the smooth scroller may decide that no work needs to be done to scroll
+      // to the element if a certain percentage of it is on screen. Ugh. Sigh. Yuck.
+
+      return isDisplayingAtLeast(FULLY_RENDERED_PERCENTAGE_CUTOFF).matches(element);
+    }
+  }
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/ClearTextAction.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/ClearTextAction.java
new file mode 100644
index 0000000..e3d997a
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/ClearTextAction.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.action;
+
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isAssignableFrom;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isDisplayed;
+import static org.hamcrest.Matchers.allOf;
+
+import com.google.android.apps.common.testing.ui.espresso.UiController;
+import com.google.android.apps.common.testing.ui.espresso.ViewAction;
+
+import android.view.View;
+import android.widget.EditText;
+
+import org.hamcrest.Matcher;
+
+/**
+ * Clears view text by setting {@link EditText}s text property to "".
+ */
+public final class ClearTextAction implements ViewAction {
+  @SuppressWarnings("unchecked")
+  @Override
+  public Matcher<View> getConstraints() {
+    return allOf(isDisplayed(), isAssignableFrom(EditText.class));
+  }
+
+  @Override
+  public void perform(UiController uiController, View view) {
+    ((EditText) view).setText("");
+  }
+
+  @Override
+  public String getDescription() {
+    return "clear text";
+  }
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/CloseKeyboardAction.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/CloseKeyboardAction.java
new file mode 100644
index 0000000..6026a68
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/CloseKeyboardAction.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.action;
+
+import static com.google.common.collect.Iterables.getOnlyElement;
+import static org.hamcrest.Matchers.anything;
+
+import com.google.android.apps.common.testing.testrunner.ActivityLifecycleMonitorRegistry;
+import com.google.android.apps.common.testing.testrunner.Stage;
+import com.google.android.apps.common.testing.ui.espresso.PerformException;
+import com.google.android.apps.common.testing.ui.espresso.UiController;
+import com.google.android.apps.common.testing.ui.espresso.ViewAction;
+import com.google.android.apps.common.testing.ui.espresso.util.HumanReadables;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.ResultReceiver;
+import android.util.Log;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+
+import org.hamcrest.Matcher;
+
+import java.util.Collection;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Closes soft keyboard.
+ */
+public final class CloseKeyboardAction implements ViewAction {
+
+  private static final int NUM_RETRIES = 3;
+  private static final String TAG = CloseKeyboardAction.class.getSimpleName();
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public Matcher<View> getConstraints() {
+    return anything();
+  }
+
+  @Override
+  public void perform(UiController uiController, View view) {
+    // Retry in case of timeout exception to avoid flakiness in IMM.
+    for (int i = 0; i < NUM_RETRIES; i++) {
+      try {
+        tryToCloseKeyboard(view, uiController);
+        return;
+      } catch (TimeoutException te) {
+        Log.w(TAG, "Caught timeout exception. Retrying.");
+        if (i == 2) {
+          throw new PerformException.Builder()
+            .withActionDescription(this.getDescription())
+            .withViewDescription(HumanReadables.describe(view))
+            .withCause(te)
+            .build();
+        }
+      }
+    }
+  }
+
+  private void tryToCloseKeyboard(View view, UiController uiController) throws TimeoutException {
+    InputMethodManager imm = (InputMethodManager) getRootActivity(uiController)
+        .getSystemService(Context.INPUT_METHOD_SERVICE);
+    final AtomicInteger atomicResultCode = new AtomicInteger();
+    final CountDownLatch latch = new CountDownLatch(1);
+
+    ResultReceiver result = new ResultReceiver(null) {
+      @Override
+      protected void onReceiveResult(int resultCode, Bundle resultData) {
+        atomicResultCode.set(resultCode);
+        latch.countDown();
+      }
+    };
+
+    if (!imm.hideSoftInputFromWindow(view.getWindowToken(), 0, result)) {
+      Log.w(TAG, "Attempting to close soft keyboard, while it is not shown.");
+      return;
+    }
+
+    try {
+      if (!latch.await(2, TimeUnit.SECONDS)) {
+        throw new TimeoutException("Wait on operation result timed out.");
+      }
+    } catch (InterruptedException e) {
+      throw new PerformException.Builder()
+        .withActionDescription(this.getDescription())
+        .withViewDescription(HumanReadables.describe(view))
+        .withCause(new RuntimeException("Waiting for soft keyboard close result was interrupted."))
+        .build();
+    }
+
+    if (atomicResultCode.get() != InputMethodManager.RESULT_UNCHANGED_HIDDEN
+        && atomicResultCode.get() != InputMethodManager.RESULT_HIDDEN) {
+      String error =
+          "Attempt to close the soft keyboard did not result in soft keyboard to be hidden."
+          + "resultCode = " + atomicResultCode.get();
+      Log.e(TAG, error);
+      throw new PerformException.Builder()
+        .withActionDescription(this.getDescription())
+        .withViewDescription(HumanReadables.describe(view))
+        .withCause(new RuntimeException(error))
+        .build();
+    }
+  }
+
+  private static Activity getRootActivity(UiController uiController) {
+    Collection<Activity> resumedActivities =
+        ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED);
+    if (resumedActivities.isEmpty()) {
+      uiController.loopMainThreadUntilIdle();
+      resumedActivities =
+          ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED);
+    }
+    Activity topActivity = getOnlyElement(resumedActivities);
+    return topActivity;
+  }
+
+  @Override
+  public String getDescription() {
+    return "close keyboard";
+  }
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/CoordinatesProvider.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/CoordinatesProvider.java
new file mode 100644
index 0000000..c8c9823
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/CoordinatesProvider.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.action;
+
+import android.view.View;
+
+/**
+ * Interface to implement calculation of Coordinates.
+ */
+public interface CoordinatesProvider {
+  
+  /**
+   * Calculates coordinates of given view.
+   * 
+   * @param view the View which is used for the calculation. 
+   * @return a float[] with x and y values of the calculated coordinates.  
+   */
+  public float[] calculateCoordinates(View view);
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/EditorAction.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/EditorAction.java
new file mode 100644
index 0000000..6b78cb9
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/EditorAction.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.action;
+
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isDisplayed;
+
+import com.google.android.apps.common.testing.ui.espresso.PerformException;
+import com.google.android.apps.common.testing.ui.espresso.UiController;
+import com.google.android.apps.common.testing.ui.espresso.ViewAction;
+import com.google.android.apps.common.testing.ui.espresso.util.HumanReadables;
+
+import android.view.View;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+
+import org.hamcrest.Matcher;
+
+/**
+ * Performs whatever editor (IME) action is available on a view.
+ */
+public final class EditorAction implements ViewAction {
+
+  @Override
+  public Matcher<View> getConstraints() {
+    return isDisplayed();
+  }
+
+  @Override
+  public String getDescription() {
+    return "input method editor";
+  }
+
+  @Override
+  public void perform(UiController uiController, View view) {
+    EditorInfo editorInfo = new EditorInfo();
+    InputConnection inputConnection = view.onCreateInputConnection(editorInfo);
+    if (inputConnection == null) {
+      throw new PerformException.Builder()
+        .withActionDescription(this.toString())
+        .withViewDescription(HumanReadables.describe(view))
+        .withCause(new IllegalStateException("View does not support input methods"))
+        .build();
+    }
+
+    int actionId = editorInfo.actionId != 0 ? editorInfo.actionId :
+      editorInfo.imeOptions & EditorInfo.IME_MASK_ACTION;
+
+    if (actionId == EditorInfo.IME_ACTION_NONE) {
+      throw new PerformException.Builder()
+        .withActionDescription(this.getDescription())
+        .withViewDescription(HumanReadables.describe(view))
+        .withCause(new IllegalStateException("No available action on view"))
+        .build();
+    }
+
+    if (!inputConnection.performEditorAction(actionId)) {
+      throw new PerformException.Builder()
+        .withActionDescription(this.getDescription())
+        .withViewDescription(HumanReadables.describe(view))
+        .withCause(new RuntimeException(String.format(
+            "Failed to perform action %#x. Input connection no longer valid", actionId)))
+        .build();
+    }
+  }
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/EspressoKey.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/EspressoKey.java
new file mode 100644
index 0000000..530ddde
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/EspressoKey.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.action;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import android.os.Build;
+import android.view.KeyEvent;
+
+/**
+ * Class that wraps the key code and meta state of the desired key press.
+ */
+public final class EspressoKey {
+  private final int keyCode;
+  private final int metaState;
+
+  private EspressoKey(Builder builder) {
+    this.keyCode = builder.builderKeyCode;
+    this.metaState = builder.getMetaState();
+  }
+
+  public int getKeyCode() {
+    return keyCode;
+  }
+
+  public int getMetaState() {
+    return metaState;
+  }
+
+  @Override
+  public String toString() {
+    return String.format("keyCode: %s, metaState: %s", keyCode, metaState);
+  }
+
+  /**
+   * Builder for the EspressoKey class.
+   */
+  public static class Builder {
+    private int builderKeyCode = -1;
+    private boolean isShiftPressed;
+    private boolean isAltPressed;
+    private boolean isCtrlPressed;
+
+    public Builder withKeyCode(int keyCode) {
+      builderKeyCode = keyCode;
+      return this;
+    }
+
+    /**
+     * Sets the SHIFT_ON meta state of the resulting key.
+     */
+    public Builder withShiftPressed(boolean shiftPressed) {
+      isShiftPressed = shiftPressed;
+      return this;
+    }
+
+    /**
+     * On Honeycomb and above, sets the CTRL_ON meta state of the resulting key. On Gingerbread and
+     * below, this is a noop.
+     */
+    public Builder withCtrlPressed(boolean ctrlPressed) {
+      isCtrlPressed = ctrlPressed;
+      return this;
+    }
+
+    /**
+     * Sets the ALT_ON meta state of the resulting key.
+     */
+    public Builder withAltPressed(boolean altPressed) {
+      isAltPressed = altPressed;
+      return this;
+    }
+
+    private int getMetaState() {
+      int metaState = 0;
+      if (isShiftPressed) {
+        metaState |= KeyEvent.META_SHIFT_ON;
+      }
+
+      if (isAltPressed) {
+        metaState |= KeyEvent.META_ALT_ON;
+      }
+
+      if (isCtrlPressed && Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+        metaState |= KeyEvent.META_CTRL_ON;
+      }
+
+      return metaState;
+    }
+
+    public EspressoKey build() {
+      checkState(builderKeyCode > 0 && builderKeyCode < KeyEvent.getMaxKeyCode(),
+          "Invalid key code: %s", builderKeyCode);
+      return new EspressoKey(this);
+    }
+  }
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/GeneralClickAction.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/GeneralClickAction.java
new file mode 100644
index 0000000..857501d
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/GeneralClickAction.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.action;
+
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isDisplayingAtLeast;
+import static org.hamcrest.Matchers.allOf;
+
+import com.google.android.apps.common.testing.ui.espresso.PerformException;
+import com.google.android.apps.common.testing.ui.espresso.UiController;
+import com.google.android.apps.common.testing.ui.espresso.ViewAction;
+import com.google.android.apps.common.testing.ui.espresso.util.HumanReadables;
+import com.google.common.base.Optional;
+
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.webkit.WebView;
+
+import org.hamcrest.Matcher;
+
+/**
+ * Enables clicking on views.
+ */
+public final class GeneralClickAction implements ViewAction {
+
+  private final CoordinatesProvider coordinatesProvider;
+  private final Tapper tapper;
+  private final PrecisionDescriber precisionDescriber;
+  private final Optional<ViewAction> rollbackAction;
+
+  public GeneralClickAction(Tapper tapper, CoordinatesProvider coordinatesProvider,
+      PrecisionDescriber precisionDescriber) {
+    this(tapper, coordinatesProvider, precisionDescriber, null);
+  }
+
+  public GeneralClickAction(Tapper tapper, CoordinatesProvider coordinatesProvider,
+      PrecisionDescriber precisionDescriber, ViewAction rollbackAction) {
+    this.coordinatesProvider = coordinatesProvider;
+    this.tapper = tapper;
+    this.precisionDescriber = precisionDescriber;
+    this.rollbackAction = Optional.fromNullable(rollbackAction);
+  }
+
+  @Override
+  @SuppressWarnings("unchecked")
+  public Matcher<View> getConstraints() {
+    Matcher<View> standardConstraint = isDisplayingAtLeast(90);
+    if (rollbackAction.isPresent()) {
+      return allOf(standardConstraint, rollbackAction.get().getConstraints());
+    } else {
+      return standardConstraint;
+    }
+  }
+
+  @Override
+  public void perform(UiController uiController, View view) {
+    float[] coordinates = coordinatesProvider.calculateCoordinates(view);
+    float[] precision = precisionDescriber.describePrecision();
+
+    Tapper.Status status = Tapper.Status.FAILURE;
+    int loopCount = 0;
+    // Native event injection is quite a tricky process. A tap is actually 2
+    // seperate motion events which need to get injected into the system. Injection
+    // makes an RPC call from our app under test to the Android system server, the
+    // system server decides which window layer to deliver the event to, the system
+    // server makes an RPC to that window layer, that window layer delivers the event
+    // to the correct UI element, activity, or window object. Now we need to repeat
+    // that 2x. for a simple down and up. Oh and the down event triggers timers to
+    // detect whether or not the event is a long vs. short press. The timers are
+    // removed the moment the up event is received (NOTE: the possibility of eventTime
+    // being in the future is totally ignored by most motion event processors).
+    //
+    // Phew.
+    //
+    // The net result of this is sometimes we'll want to do a regular tap, and for
+    // whatever reason the up event (last half) of the tap is delivered after long
+    // press timeout (depending on system load) and the long press behaviour is
+    // displayed (EG: show a context menu). There is no way to avoid or handle this more
+    // gracefully. Also the longpress behavour is app/widget specific. So if you have
+    // a seperate long press behaviour from your short press, you can pass in a
+    // 'RollBack' ViewAction which when executed will undo the effects of long press.
+
+    while (status != Tapper.Status.SUCCESS && loopCount < 3) {
+      try {
+        status = tapper.sendTap(uiController, coordinates, precision);
+      } catch (RuntimeException re) {
+        throw new PerformException.Builder()
+            .withActionDescription(this.getDescription())
+            .withViewDescription(HumanReadables.describe(view))
+            .withCause(re)
+            .build();
+      }
+
+      // ensures that all work enqueued to process the tap has been run.
+      uiController.loopMainThreadForAtLeast(ViewConfiguration.getPressedStateDuration());
+      if (status == Tapper.Status.WARNING) {
+        if (rollbackAction.isPresent()) {
+          rollbackAction.get().perform(uiController, view);
+        } else {
+          break;
+        }
+      }
+      loopCount++;
+    }
+    if (status == Tapper.Status.FAILURE) {
+      throw new PerformException.Builder()
+        .withActionDescription(this.getDescription())
+        .withViewDescription(HumanReadables.describe(view))
+        .withCause(new RuntimeException(String.format("Couldn't "
+            + "click at: %s,%s precision: %s, %s . Tapper: %s coordinate provider: %s precision " +
+            "describer: %s. Tried %s times. With Rollback? %s", coordinates[0], coordinates[1],
+            precision[0], precision[1], tapper, coordinatesProvider, precisionDescriber, loopCount,
+            rollbackAction.isPresent())))
+        .build();
+    }
+
+    if (tapper == Tap.SINGLE && view instanceof WebView) {
+      // WebViews will not process click events until double tap
+      // timeout. Not the best place for this - but good for now.
+      uiController.loopMainThreadForAtLeast(ViewConfiguration.getDoubleTapTimeout());
+    }
+  }
+
+  @Override
+  public String getDescription() {
+    return tapper.toString().toLowerCase() + " click";
+  }
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/GeneralLocation.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/GeneralLocation.java
new file mode 100644
index 0000000..f74775e
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/GeneralLocation.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.action;
+
+import android.view.View;
+
+/**
+ * Calculates coordinate position for general locations.
+ */
+public enum GeneralLocation implements CoordinatesProvider {
+
+  TOP_LEFT {
+  @Override
+    public float[] calculateCoordinates(View view) {
+      return getCoordinates(view, Position.BEGIN, Position.BEGIN);
+    }
+  },
+  TOP_CENTER {
+  @Override
+    public float[] calculateCoordinates(View view) {
+      return getCoordinates(view, Position.BEGIN, Position.MIDDLE);
+    }
+  },
+  TOP_RIGHT {
+  @Override
+    public float[] calculateCoordinates(View view) {
+      return getCoordinates(view, Position.BEGIN, Position.END);
+    }
+  },
+  CENTER_LEFT {
+  @Override
+    public float[] calculateCoordinates(View view) {
+      return getCoordinates(view, Position.MIDDLE, Position.BEGIN);
+    }
+  },
+  CENTER {
+  @Override
+    public float[] calculateCoordinates(View view) {
+      return getCoordinates(view, Position.MIDDLE, Position.MIDDLE);
+    }
+  },
+  CENTER_RIGHT {
+  @Override
+    public float[] calculateCoordinates(View view) {
+      return getCoordinates(view, Position.MIDDLE, Position.END);
+    }
+  },
+  BOTTOM_LEFT {
+  @Override
+    public float[] calculateCoordinates(View view) {
+      return getCoordinates(view, Position.END, Position.BEGIN);
+    }
+  },
+  BOTTOM_CENTER {
+  @Override
+    public float[] calculateCoordinates(View view) {
+      return getCoordinates(view, Position.END, Position.MIDDLE);
+    }
+  },
+  BOTTOM_RIGHT {
+  @Override
+    public float[] calculateCoordinates(View view) {
+      return getCoordinates(view, Position.END, Position.END);
+    }
+  };
+
+  private static float[] getCoordinates(View view, Position vertical, Position horizontal) {
+    final int[] xy = new int[2];
+    view.getLocationOnScreen(xy);
+    final float x = horizontal.getPosition(xy[0], view.getWidth());
+    final float y = vertical.getPosition(xy[1], view.getHeight());
+    float[] coordinates = {x, y};
+    return coordinates;
+  }
+
+  private static enum Position {
+    BEGIN {
+    @Override
+      public float getPosition(int viewPos, int viewLength) {
+        return viewPos;
+      }
+    },
+    MIDDLE {
+    @Override
+      public float getPosition(int viewPos, int viewLength) {
+        return viewPos + (viewLength / 2.0f);
+      }
+    },
+    END {
+    @Override
+      public float getPosition(int viewPos, int viewLength) {
+        return viewPos + viewLength;
+      }
+    };
+
+    abstract float getPosition(int widgetPos, int widgetLength);
+  }
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/GeneralSwipeAction.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/GeneralSwipeAction.java
new file mode 100644
index 0000000..6482250
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/GeneralSwipeAction.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.action;
+
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isDisplayingAtLeast;
+
+import com.google.android.apps.common.testing.ui.espresso.PerformException;
+import com.google.android.apps.common.testing.ui.espresso.UiController;
+import com.google.android.apps.common.testing.ui.espresso.ViewAction;
+import com.google.android.apps.common.testing.ui.espresso.util.HumanReadables;
+
+import android.view.View;
+import android.view.ViewConfiguration;
+
+import org.hamcrest.Matcher;
+
+/**
+ * Enables swiping across a view.
+ */
+public final class GeneralSwipeAction implements ViewAction {
+
+  /** Maximum number of times to attempt sending a swipe action. */
+  private static final int MAX_TRIES = 3;
+
+  /** The minimum amount of a view that must be displayed in order to swipe across it. */
+  private static final int VIEW_DISPLAY_PERCENTAGE = 90;
+
+  private final CoordinatesProvider startCoordinatesProvider;
+  private final CoordinatesProvider endCoordinatesProvider;
+  private final Swiper swiper;
+  private final PrecisionDescriber precisionDescriber;
+
+  public GeneralSwipeAction(Swiper swiper, CoordinatesProvider startCoordinatesProvider,
+      CoordinatesProvider endCoordinatesProvider, PrecisionDescriber precisionDescriber) {
+    this.swiper = swiper;
+    this.startCoordinatesProvider = startCoordinatesProvider;
+    this.endCoordinatesProvider = endCoordinatesProvider;
+    this.precisionDescriber = precisionDescriber;
+  }
+
+  @Override
+  public Matcher<View> getConstraints() {
+    return isDisplayingAtLeast(VIEW_DISPLAY_PERCENTAGE);
+  }
+
+  @Override
+  public void perform(UiController uiController, View view) {
+    float[] startCoordinates = startCoordinatesProvider.calculateCoordinates(view);
+    float[] endCoordinates = endCoordinatesProvider.calculateCoordinates(view);
+    float[] precision = precisionDescriber.describePrecision();
+
+    Swiper.Status status = Swiper.Status.FAILURE;
+
+    for (int tries = 0; tries < MAX_TRIES && status != Swiper.Status.SUCCESS; tries++) {
+      try {
+        status = swiper.sendSwipe(uiController, startCoordinates, endCoordinates, precision);
+      } catch (RuntimeException re) {
+        throw new PerformException.Builder()
+            .withActionDescription(this.getDescription())
+            .withViewDescription(HumanReadables.describe(view))
+            .withCause(re)
+            .build();
+      }
+
+      // ensures that all work enqueued to process the swipe has been run.
+      uiController.loopMainThreadForAtLeast(ViewConfiguration.getPressedStateDuration());
+    }
+
+    if (status == Swiper.Status.FAILURE) {
+      throw new PerformException.Builder()
+          .withActionDescription(getDescription())
+          .withViewDescription(HumanReadables.describe(view))
+          .withCause(new RuntimeException(String.format(
+              "Couldn't swipe from: %s,%s to: %s,%s precision: %s, %s . Swiper: %s "
+              + "start coordinate provider: %s precision describer: %s. Tried %s times",
+              startCoordinates[0],
+              startCoordinates[1],
+              endCoordinates[0],
+              endCoordinates[1],
+              precision[0],
+              precision[1],
+              swiper,
+              startCoordinatesProvider,
+              precisionDescriber,
+              MAX_TRIES)))
+          .build();
+    }
+  }
+
+  @Override
+  public String getDescription() {
+    return swiper.toString().toLowerCase() + " swipe";
+  }
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/KeyEventAction.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/KeyEventAction.java
new file mode 100644
index 0000000..1be85f7
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/KeyEventAction.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.action;
+
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isDisplayed;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.android.apps.common.testing.testrunner.ActivityLifecycleMonitorRegistry;
+import com.google.android.apps.common.testing.testrunner.Stage;
+import com.google.android.apps.common.testing.ui.espresso.InjectEventSecurityException;
+import com.google.android.apps.common.testing.ui.espresso.NoActivityResumedException;
+import com.google.android.apps.common.testing.ui.espresso.PerformException;
+import com.google.android.apps.common.testing.ui.espresso.UiController;
+import com.google.android.apps.common.testing.ui.espresso.ViewAction;
+import com.google.android.apps.common.testing.ui.espresso.util.HumanReadables;
+
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+
+import org.hamcrest.Matcher;
+
+/**
+ * Enables pressing KeyEvents on views.
+ */
+public final class KeyEventAction implements ViewAction {
+  private static final String TAG = KeyEventAction.class.getSimpleName();
+
+  private final EspressoKey key;
+
+  public KeyEventAction(EspressoKey key) {
+    this.key = checkNotNull(key);
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public Matcher<View> getConstraints() {
+    return isDisplayed();
+  }
+
+  @Override
+  public void perform(UiController uiController, View view) {
+    try {
+      if (!sendKeyEvent(uiController, view)) {
+        Log.e(TAG, "Failed to inject key event: " + this.key);
+        throw new PerformException.Builder()
+          .withActionDescription(this.getDescription())
+          .withViewDescription(HumanReadables.describe(view))
+          .withCause(new RuntimeException("Failed to inject key event " + this.key))
+          .build();
+      }
+    } catch (InjectEventSecurityException e) {
+      Log.e(TAG, "Failed to inject key event: " + this.key);
+      throw new PerformException.Builder()
+        .withActionDescription(this.getDescription())
+        .withViewDescription(HumanReadables.describe(view))
+        .withCause(e)
+        .build();
+    }
+  }
+
+  private final boolean sendKeyEvent(UiController controller, View view)
+      throws InjectEventSecurityException {
+
+    boolean injected = false;
+    long eventTime = SystemClock.uptimeMillis();
+    for (int attempts = 0; !injected && attempts < 4; attempts++) {
+      injected = controller.injectKeyEvent(new KeyEvent(eventTime,
+          eventTime,
+          KeyEvent.ACTION_DOWN,
+          this.key.getKeyCode(),
+          0,
+          this.key.getMetaState()));
+    }
+
+    if (!injected) {
+      // it is not a transient failure... :(
+      return false;
+    }
+
+    injected = false;
+    eventTime = SystemClock.uptimeMillis();
+    for (int attempts = 0; !injected && attempts < 4; attempts++) {
+      injected = controller.injectKeyEvent(
+          new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, this.key.getKeyCode(), 0));
+    }
+
+
+    if (this.key.getKeyCode() == KeyEvent.KEYCODE_BACK) {
+      controller.loopMainThreadUntilIdle();
+      boolean activeActivities = !ActivityLifecycleMonitorRegistry.getInstance()
+          .getActivitiesInStage(Stage.RESUMED)
+          .isEmpty();
+      if (!activeActivities) {
+        Throwable cause = new PerformException.Builder()
+          .withActionDescription(this.getDescription())
+          .withViewDescription(HumanReadables.describe(view))
+          .build();
+        throw new NoActivityResumedException("Pressed back and killed the app", cause);
+      }
+    }
+
+    return injected;
+  }
+
+  @Override
+  public String getDescription() {
+    return String.format("send %s key event", this.key);
+  }
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/MotionEvents.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/MotionEvents.java
new file mode 100644
index 0000000..1f38339
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/MotionEvents.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.action;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.android.apps.common.testing.testrunner.UsageTrackerRegistry;
+import com.google.android.apps.common.testing.ui.espresso.InjectEventSecurityException;
+import com.google.android.apps.common.testing.ui.espresso.PerformException;
+import com.google.android.apps.common.testing.ui.espresso.UiController;
+import com.google.common.annotations.VisibleForTesting;
+
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+
+/**
+ * Facilitates sending of motion events to a {@link UiController}.
+ */
+final class MotionEvents {
+
+  private static final String TAG = MotionEvents.class.getSimpleName();
+
+  @VisibleForTesting
+  static final int MAX_CLICK_ATTEMPTS = 3;
+
+  private MotionEvents() {
+    // Shouldn't be instantiated
+  }
+
+  static DownResultHolder sendDown(
+      UiController uiController, float[] coordinates, float[] precision) {
+    checkNotNull(uiController);
+    checkNotNull(coordinates);
+    checkNotNull(precision);
+
+    for (int retry = 0; retry < MAX_CLICK_ATTEMPTS; retry++) {
+      MotionEvent motionEvent = null;
+      try {
+        // Algorithm of sending click event adopted from android.test.TouchUtils.
+        // When the click event was first initiated. Needs to be same for both down and up press
+        // events.
+        long downTime = SystemClock.uptimeMillis();
+
+        // Down press.
+        motionEvent = MotionEvent.obtain(downTime,
+            SystemClock.uptimeMillis(),
+            MotionEvent.ACTION_DOWN,
+            coordinates[0],
+            coordinates[1],
+            0, // pressure
+            1, // size
+            0, // metaState
+            precision[0], // xPrecision
+            precision[1], // yPrecision
+            0,  // deviceId
+            0); // edgeFlags
+        // The down event should be considered a tap if it is long enough to be detected
+        // but short enough not to be a long-press. Assume that TapTimeout is set at least
+        // twice the detection time for a tap (no need to sleep for the whole TapTimeout since
+        // we aren't concerned about scrolling here).
+        long isTapAt = downTime + (ViewConfiguration.getTapTimeout() / 2);
+
+        boolean injectEventSucceeded = uiController.injectMotionEvent(motionEvent);
+
+        while (true) {
+          long delayToBeTap = isTapAt - SystemClock.uptimeMillis();
+          if (delayToBeTap <= 10) {
+            break;
+          }
+          // Sleep only a fraction of the time, since there may be other events in the UI queue
+          // that could cause us to start sleeping late, and then oversleep.
+          uiController.loopMainThreadForAtLeast(delayToBeTap / 4);
+        }
+
+        boolean longPress = false;
+        if (SystemClock.uptimeMillis() > (downTime + ViewConfiguration.getLongPressTimeout())) {
+          longPress = true;
+          Log.e(TAG, "Overslept and turned a tap into a long press");
+          UsageTrackerRegistry.getInstance().trackUsage("Espresso.Tap.Error.tapToLongPress");
+        }
+
+        if (!injectEventSucceeded) {
+          motionEvent.recycle();
+          motionEvent = null;
+          continue;
+        }
+
+        return new DownResultHolder(motionEvent, longPress);
+      } catch (InjectEventSecurityException e) {
+        throw new PerformException.Builder()
+          .withActionDescription("Send down montion event")
+          .withViewDescription("unknown") // likely to be replaced by FailureHandler
+          .withCause(e)
+          .build();
+      }
+    }
+    throw new PerformException.Builder()
+      .withActionDescription(String.format("click (after %s attempts)", MAX_CLICK_ATTEMPTS))
+      .withViewDescription("unknown") // likely to be replaced by FailureHandler
+      .build();
+  }
+
+  static boolean sendUp(UiController uiController, MotionEvent downEvent) {
+    return sendUp(uiController, downEvent, new float[] { downEvent.getX(), downEvent.getY() });
+  }
+
+  static boolean sendUp(UiController uiController, MotionEvent downEvent, float[] coordinates) {
+    checkNotNull(uiController);
+    checkNotNull(downEvent);
+    checkNotNull(coordinates);
+
+    MotionEvent motionEvent = null;
+    try {
+      // Up press.
+      motionEvent = MotionEvent.obtain(downEvent.getDownTime(),
+          SystemClock.uptimeMillis(),
+          MotionEvent.ACTION_UP,
+          coordinates[0],
+          coordinates[1],
+          0);
+      boolean injectEventSucceeded = uiController.injectMotionEvent(motionEvent);
+
+      if (!injectEventSucceeded) {
+        Log.e(TAG, String.format(
+            "Injection of up event failed (corresponding down event: %s)", downEvent.toString()));
+        return false;
+      }
+    } catch (InjectEventSecurityException e) {
+      throw new PerformException.Builder()
+        .withActionDescription(
+            String.format("inject up event (corresponding down event: %s)", downEvent.toString()))
+        .withViewDescription("unknown") // likely to be replaced by FailureHandler
+        .withCause(e)
+        .build();
+    } finally {
+      if (null != motionEvent) {
+        motionEvent.recycle();
+        motionEvent = null;
+      }
+    }
+    return true;
+  }
+
+  static void sendCancel(UiController uiController, MotionEvent downEvent) {
+    checkNotNull(uiController);
+    checkNotNull(downEvent);
+
+    MotionEvent motionEvent = null;
+    try {
+      // Up press.
+      motionEvent = MotionEvent.obtain(downEvent.getDownTime(),
+          SystemClock.uptimeMillis(),
+          MotionEvent.ACTION_CANCEL,
+          downEvent.getX(),
+          downEvent.getY(),
+          0);
+      boolean injectEventSucceeded = uiController.injectMotionEvent(motionEvent);
+
+      if (!injectEventSucceeded) {
+        throw new PerformException.Builder()
+          .withActionDescription(String.format(
+            "inject cancel event (corresponding down event: %s)", downEvent.toString()))
+          .withViewDescription("unknown") // likely to be replaced by FailureHandler
+          .build();
+      }
+    } catch (InjectEventSecurityException e) {
+      throw new PerformException.Builder()
+        .withActionDescription(String.format(
+          "inject cancel event (corresponding down event: %s)", downEvent.toString()))
+        .withViewDescription("unknown") // likely to be replaced by FailureHandler
+        .withCause(e)
+        .build();
+    } finally {
+      if (null != motionEvent) {
+        motionEvent.recycle();
+        motionEvent = null;
+      }
+    }
+  }
+
+  static boolean sendMovement(UiController uiController, MotionEvent downEvent,
+      float[] coordinates) {
+    checkNotNull(uiController);
+    checkNotNull(downEvent);
+    checkNotNull(coordinates);
+
+    MotionEvent motionEvent = null;
+    try {
+      motionEvent = MotionEvent.obtain(downEvent.getDownTime(),
+          SystemClock.uptimeMillis(),
+          MotionEvent.ACTION_MOVE,
+          coordinates[0],
+          coordinates[1],
+          0);
+      boolean injectEventSucceeded = uiController.injectMotionEvent(motionEvent);
+
+      if (!injectEventSucceeded) {
+        Log.e(TAG, String.format(
+            "Injection of motion event failed (corresponding down event: %s)",
+            downEvent.toString()));
+        return false;
+      }
+    } catch (InjectEventSecurityException e) {
+      throw new PerformException.Builder()
+        .withActionDescription(String.format(
+          "inject motion event (corresponding down event: %s)", downEvent.toString()))
+        .withViewDescription("unknown") // likely to be replaced by FailureHandler
+        .withCause(e)
+        .build();
+    } finally {
+      if (null != motionEvent) {
+        motionEvent.recycle();
+        motionEvent = null;
+      }
+    }
+
+    return true;
+  }
+
+  /**
+   * Holds the result of a down motion.
+   */
+  static class DownResultHolder {
+    public final MotionEvent down;
+    public final boolean longPress;
+
+    DownResultHolder(MotionEvent down, boolean longPress) {
+      this.down = down;
+      this.longPress = longPress;
+    }
+  }
+
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/PrecisionDescriber.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/PrecisionDescriber.java
new file mode 100644
index 0000000..422de8e
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/PrecisionDescriber.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.action;
+
+/**
+ * Interface to implement size of click area.
+ */
+public interface PrecisionDescriber {
+
+  /**
+   * Different touch target sizes.
+   *
+   * @return a float[] with x and y values of size of click area.
+   */
+  public float[] describePrecision();
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/Press.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/Press.java
new file mode 100644
index 0000000..883c852
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/Press.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.action;
+
+/**
+ * Returns different touch target sizes.
+ */
+public enum Press implements PrecisionDescriber {
+  PINPOINT {
+  @Override
+    public float[] describePrecision() {
+      float[] pinpoint = {1f, 1f};
+      return pinpoint;
+    }
+  },
+  FINGER {
+    // average width of the index finger is 16 – 20 mm.
+  @Override
+    public float[] describePrecision() {
+      float finger[] = {16f, 16f};
+      return finger;
+    }
+  },
+  // average width of an adult thumb is 25 mm (1 inch).
+  THUMB {
+  @Override
+    public float[] describePrecision() {
+      float thumb[] = {25f, 25f};
+      return thumb;
+    }
+  };
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/ScrollToAction.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/ScrollToAction.java
new file mode 100644
index 0000000..9d613c3
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/ScrollToAction.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.action;
+
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isAssignableFrom;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isDescendantOfA;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isDisplayingAtLeast;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withEffectiveVisibility;
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.anyOf;
+
+import com.google.android.apps.common.testing.ui.espresso.PerformException;
+import com.google.android.apps.common.testing.ui.espresso.UiController;
+import com.google.android.apps.common.testing.ui.espresso.ViewAction;
+import com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.Visibility;
+import com.google.android.apps.common.testing.ui.espresso.util.HumanReadables;
+
+import android.graphics.Rect;
+import android.util.Log;
+import android.view.View;
+import android.widget.HorizontalScrollView;
+import android.widget.ScrollView;
+
+import org.hamcrest.Matcher;
+
+/**
+ * Enables scrolling to the given view. View must be a descendant of a ScrollView.
+ */
+public final class ScrollToAction implements ViewAction {
+  private static final String TAG = ScrollToAction.class.getSimpleName();
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public Matcher<View> getConstraints() {
+    return allOf(withEffectiveVisibility(Visibility.VISIBLE), isDescendantOfA(anyOf(
+        isAssignableFrom(ScrollView.class), isAssignableFrom(HorizontalScrollView.class))));
+  }
+
+  @Override
+  public void perform(UiController uiController, View view) {
+    if (isDisplayingAtLeast(90).matches(view)) {
+      Log.i(TAG, "View is already displayed. Returning.");
+      return;
+    }
+    Rect rect = new Rect();
+    view.getDrawingRect(rect);
+    if (!view.requestRectangleOnScreen(rect, true /* immediate */)) {
+      Log.w(TAG, "Scrolling to view was requested, but none of the parents scrolled.");
+    }
+    uiController.loopMainThreadUntilIdle();
+    if (!isDisplayingAtLeast(90).matches(view)) {
+      throw new PerformException.Builder()
+        .withActionDescription(this.getDescription())
+        .withViewDescription(HumanReadables.describe(view))
+        .withCause(new RuntimeException(
+              "Scrolling to view was attempted, but the view is not displayed"))
+        .build();
+    }
+  }
+
+  @Override
+  public String getDescription() {
+    return "scroll to";
+  }
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/Swipe.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/Swipe.java
new file mode 100644
index 0000000..06e8dc8
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/Swipe.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.action;
+
+import static com.google.common.base.Preconditions.checkElementIndex;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.android.apps.common.testing.ui.espresso.UiController;
+
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.MotionEvent;
+
+/**
+ * Executes different swipe types to given positions.
+ */
+public enum Swipe implements Swiper {
+
+  /** Swipes quickly between the co-ordinates. */
+  FAST {
+  @Override
+    public Swiper.Status sendSwipe(UiController uiController, float[] startCoordinates,
+        float[] endCoordinates, float[] precision) {
+      return sendLinearSwipe(uiController, startCoordinates, endCoordinates, precision,
+          SWIPE_FAST_DURATION_MS);
+    }
+  },
+
+  /** Swipes deliberately slowly between the co-ordinates, to aid in visual debugging. */
+  SLOW {
+  @Override
+    public Swiper.Status sendSwipe(UiController uiController, float[] startCoordinates,
+        float[] endCoordinates, float[] precision) {
+      return sendLinearSwipe(uiController, startCoordinates, endCoordinates, precision,
+          SWIPE_SLOW_DURATION_MS);
+    }
+  };
+
+  private static final String TAG = Swipe.class.getSimpleName();
+
+  /** The number of motion events to send for each swipe. */
+  private static final int SWIPE_EVENT_COUNT = 10;
+
+  /** Length of time a "fast" swipe should last for, in milliseconds. */
+  private static final int SWIPE_FAST_DURATION_MS = 100;
+
+  /** Length of time a "slow" swipe should last for, in milliseconds. */
+  private static final int SWIPE_SLOW_DURATION_MS = 1500;
+
+  private static float[][] interpolate(float[] start, float[] end, int steps) {
+    checkElementIndex(1, start.length);
+    checkElementIndex(1, end.length);
+
+    float[][] res = new float[steps][2];
+
+    for (int i = 1; i < steps + 1; i++) {
+      res[i - 1][0] = start[0] + (end[0] - start[0]) * i / (steps + 2f);
+      res[i - 1][1] = start[1] + (end[1] - start[1]) * i / (steps + 2f);
+    }
+
+    return res;
+  }
+
+  private static Swiper.Status sendLinearSwipe(UiController uiController, float[] startCoordinates,
+      float[] endCoordinates, float[] precision, int duration) {
+    checkNotNull(uiController);
+    checkNotNull(startCoordinates);
+    checkNotNull(endCoordinates);
+    checkNotNull(precision);
+
+    float[][] steps = interpolate(startCoordinates, endCoordinates, SWIPE_EVENT_COUNT);
+    final int delayBetweenMovements = duration / steps.length;
+
+    MotionEvent downEvent = MotionEvents.sendDown(uiController, steps[0], precision).down;
+    try {
+      for (int i = 1; i < steps.length; i++) {
+        if (!MotionEvents.sendMovement(uiController, downEvent, steps[i])) {
+          Log.e(TAG, "Injection of move event as part of the swipe failed. Sending cancel event.");
+          MotionEvents.sendCancel(uiController, downEvent);
+          return Swiper.Status.FAILURE;
+        }
+
+        long desiredTime = downEvent.getDownTime() + delayBetweenMovements * i;
+        long timeUntilDesired = desiredTime - SystemClock.uptimeMillis();
+        if (timeUntilDesired > 10) {
+          uiController.loopMainThreadForAtLeast(timeUntilDesired);
+        }
+      }
+
+      if (!MotionEvents.sendUp(uiController, downEvent, endCoordinates)) {
+        Log.e(TAG, "Injection of up event as part of the swipe failed. Sending cancel event.");
+        MotionEvents.sendCancel(uiController, downEvent);
+        return Swiper.Status.FAILURE;
+      }
+    } finally {
+      downEvent.recycle();
+    }
+    return Swiper.Status.SUCCESS;
+  }
+
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/Swiper.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/Swiper.java
new file mode 100644
index 0000000..41ac593
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/Swiper.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.action;
+
+import com.google.android.apps.common.testing.ui.espresso.UiController;
+
+import android.view.MotionEvent;
+
+/**
+ * Interface to implement different swipe types.
+ */
+public interface Swiper {
+
+  /**
+   * The result of the swipe.
+   */
+  public enum Status {
+    /**
+     * The swipe action completed successfully.
+     */
+    SUCCESS,
+    /**
+     * Injecting the event was a complete failure.
+     */
+    FAILURE
+  }
+
+  /**
+   * Swipes from {@code startCoordinates} to {@code endCoordinates} using the given
+   * {@code uiController} to send {@link MotionEvent}s.
+   *
+   * @param uiController a UiController to use to send MotionEvents to the screen.
+   * @param startCoordinates a float[] with x and y co-ordinates of the start of the swipe.
+   * @param endCoordinates a float[] with x and y co-ordinates of the end of the swipe.
+   * @param precision a float[] with x and y values of precision of the tap.
+   * @return The status of the swipe.
+   */
+  public Status sendSwipe(UiController uiController, float[] startCoordinates,
+          float[] endCoordinates, float[] precision);
+
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/Tap.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/Tap.java
new file mode 100644
index 0000000..712f8cc
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/Tap.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.action;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.android.apps.common.testing.ui.espresso.UiController;
+import com.google.android.apps.common.testing.ui.espresso.action.MotionEvents.DownResultHolder;
+
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+
+/**
+ * Executes different click types to given position.
+ */
+public enum Tap implements Tapper {
+  SINGLE {
+  @Override
+    public Tapper.Status sendTap(UiController uiController, float[] coordinates,
+        float[] precision) {
+      checkNotNull(uiController);
+
+      checkNotNull(coordinates);
+      checkNotNull(precision);
+      DownResultHolder res = MotionEvents.sendDown(uiController, coordinates, precision);
+      try {
+        if (!MotionEvents.sendUp(uiController, res.down)) {
+          Log.d(TAG, "Injection of up event as part of the click failed. Send cancel event.");
+          MotionEvents.sendCancel(uiController, res.down);
+          return Tapper.Status.FAILURE;
+        }
+      } finally {
+        res.down.recycle();
+      }
+      return res.longPress ? Tapper.Status.WARNING : Tapper.Status.SUCCESS;
+    }
+  },
+  LONG {
+  @Override
+    public Tapper.Status sendTap(UiController uiController, float[] coordinates,
+        float[] precision) {
+      checkNotNull(uiController);
+      checkNotNull(coordinates);
+      checkNotNull(precision);
+
+      MotionEvent downEvent = MotionEvents.sendDown(uiController, coordinates, precision).down;
+      try {
+        // Duration before a press turns into a long press.
+        // Factor 1.5 is needed, otherwise a long press is not safely detected.
+        // See android.test.TouchUtils longClickView
+        long longPressTimeout = (long) (ViewConfiguration.getLongPressTimeout() * 1.5f);
+        uiController.loopMainThreadForAtLeast(longPressTimeout);
+
+        if (!MotionEvents.sendUp(uiController, downEvent)) {
+          MotionEvents.sendCancel(uiController, downEvent);
+          return Tapper.Status.FAILURE;
+        }
+      } finally {
+        downEvent.recycle();
+        downEvent = null;
+      }
+      return Tapper.Status.SUCCESS;
+    }
+  },
+  DOUBLE {
+  @Override
+    public Tapper.Status sendTap(UiController uiController, float[] coordinates,
+        float[] precision) {
+      checkNotNull(uiController);
+      checkNotNull(coordinates);
+      checkNotNull(precision);
+      Tapper.Status stat = SINGLE.sendTap(uiController, coordinates, precision);
+      if (stat == Tapper.Status.FAILURE) {
+        return Tapper.Status.FAILURE;
+      }
+
+      Tapper.Status secondStat = SINGLE.sendTap(uiController, coordinates, precision);
+
+      if (secondStat == Tapper.Status.FAILURE) {
+        return Tapper.Status.FAILURE;
+      }
+
+      if (secondStat == Tapper.Status.WARNING || stat == Tapper.Status.WARNING) {
+        return Tapper.Status.WARNING;
+      } else {
+        return Tapper.Status.SUCCESS;
+      }
+    }
+  };
+
+  private static final String TAG = Tap.class.getSimpleName();
+
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/Tapper.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/Tapper.java
new file mode 100644
index 0000000..8a57c53
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/Tapper.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.action;
+
+import com.google.android.apps.common.testing.ui.espresso.UiController;
+
+/**
+ * Interface to implement different click types.
+ */
+public interface Tapper {
+
+  /**
+   * The result of the tap.
+   */
+  public enum Status {
+    /**
+     * The Tap action completed successfully.
+     */
+    SUCCESS,
+    /**
+     * The action seemed to have completed - but may have been misinterpreted
+     * by the application. (For Example a TAP became a LONG PRESS by measuring
+     * its time between the down and up events).
+     */
+    WARNING,
+    /**
+     * Injecting the event was a complete failure.
+     */
+    FAILURE }
+
+  /**
+   * Sends a MotionEvent to the given UiController.
+   *
+   * @param uiController a UiController to use to send MotionEvents to the screen.
+   * @param coordinates a float[] with x and y values of center of the tap.
+   * @param precision  a float[] with x and y values of precision of the tap.
+   * @return The status of the tap.
+   */
+  public Status sendTap(UiController uiController, float[] coordinates, float[] precision);
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/TypeTextAction.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/TypeTextAction.java
new file mode 100644
index 0000000..81d388b
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/TypeTextAction.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.action;
+
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.hasFocus;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isAssignableFrom;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isDisplayed;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.supportsInputMethods;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.anyOf;
+
+import com.google.android.apps.common.testing.ui.espresso.InjectEventSecurityException;
+import com.google.android.apps.common.testing.ui.espresso.PerformException;
+import com.google.android.apps.common.testing.ui.espresso.UiController;
+import com.google.android.apps.common.testing.ui.espresso.ViewAction;
+import com.google.android.apps.common.testing.ui.espresso.util.HumanReadables;
+
+import android.os.Build;
+import android.util.Log;
+import android.view.View;
+import android.widget.SearchView;
+
+import org.hamcrest.Matcher;
+
+/**
+ * Enables typing text on views.
+ */
+public final class TypeTextAction implements ViewAction {
+  private static final String TAG = TypeTextAction.class.getSimpleName();
+  private final String stringToBeTyped;
+  private final boolean tapToFocus;
+
+  /**
+   * Constructs {@link TypeTextAction} with given string. If the string is empty it results in no-op
+   * (nothing is typed). By default this action sends a tap event to the center of the view to 
+   * attain focus before typing.
+   *
+   * @param stringToBeTyped String To be typed by {@link TypeTextAction}
+   */
+  public TypeTextAction(String stringToBeTyped) {
+    this(stringToBeTyped, true);
+  }
+
+  /**
+   * Constructs {@link TypeTextAction} with given string. If the string is empty it results in no-op
+   * (nothing is typed).
+   *
+   * @param stringToBeTyped String To be typed by {@link TypeTextAction}
+   * @param tapToFocus indicates whether a tap should be sent to the underlying view before typing.
+   */
+  public TypeTextAction(String stringToBeTyped, boolean tapToFocus) {
+    checkNotNull(stringToBeTyped);
+    this.stringToBeTyped = stringToBeTyped;
+    this.tapToFocus = tapToFocus;
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public Matcher<View> getConstraints() {
+    Matcher<View> matchers = allOf(isDisplayed());
+    if (!tapToFocus) {
+      matchers = allOf(matchers, hasFocus());
+    }
+
+    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
+       return allOf(matchers, supportsInputMethods());
+    } else {
+       // SearchView does not support input methods itself (rather it delegates to an internal text
+       // view for input).
+       return allOf(matchers, anyOf(supportsInputMethods(), isAssignableFrom(SearchView.class)));
+    }
+  }
+
+  @Override
+  public void perform(UiController uiController, View view) {
+    // No-op if string is empty.
+    if (stringToBeTyped.length() == 0) {
+      Log.w(TAG, "Supplied string is empty resulting in no-op (nothing is typed).");
+      return;
+    }
+
+    if (tapToFocus) {
+      // Perform a click.
+      new GeneralClickAction(Tap.SINGLE, GeneralLocation.CENTER, Press.FINGER)
+          .perform(uiController, view);
+      uiController.loopMainThreadUntilIdle();
+    }
+
+    try {
+      if (!uiController.injectString(stringToBeTyped)) {
+        Log.e(TAG, "Failed to type text: " + stringToBeTyped);
+        throw new PerformException.Builder()
+          .withActionDescription(this.getDescription())
+          .withViewDescription(HumanReadables.describe(view))
+          .withCause(new RuntimeException("Failed to type text: " + stringToBeTyped))
+          .build();
+      }
+    } catch (InjectEventSecurityException e) {
+      Log.e(TAG, "Failed to type text: " + stringToBeTyped);
+      throw new PerformException.Builder()
+        .withActionDescription(this.getDescription())
+        .withViewDescription(HumanReadables.describe(view))
+        .withCause(e)
+        .build();
+    }
+  }
+
+  @Override
+  public String getDescription() {
+    return "type text";
+  }
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/ViewActions.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/ViewActions.java
new file mode 100644
index 0000000..07b0e3d
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/ViewActions.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.action;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.android.apps.common.testing.ui.espresso.ViewAction;
+
+import android.view.KeyEvent;
+
+/**
+ * A collection of common {@link ViewActions}.
+ */
+public final class ViewActions {
+
+  private ViewActions() {}
+
+  /**
+   * Returns an action that clears text on the view.<br>
+   * <br>
+   * View constraints:
+   * <ul>
+   * <li>must be displayed on screen
+   * <ul>
+   */
+  public static ViewAction clearText() {
+    return new ClearTextAction();
+  }
+
+  /**
+   * Returns an action that clicks the view.<br>
+   * <br>
+   * View constraints:
+   * <ul>
+   * <li>must be displayed on screen
+   * <ul>
+   */
+  public static ViewAction click() {
+    return new GeneralClickAction(Tap.SINGLE, GeneralLocation.CENTER, Press.FINGER);
+  }
+
+  /**
+   * Returns an action that performs a single click on the view.
+   *
+   * If the click takes longer than the 'long press' duration (which is possible) the provided
+   * rollback action is invoked on the view and a click is attempted again.
+   *
+   * This is only necessary if the view being clicked on has some different behaviour for long press
+   * versus a normal tap.
+   *
+   * For example - if a long press on a particular view element opens a popup menu -
+   * ViewActions.pressBack() may be an acceptable rollback action.
+   *
+   * <br>
+   * View constraints:
+   * <ul>
+   * <li>must be displayed on screen</li>
+   * <li>any constraints of the rollbackAction</li>
+   * <ul>
+   */
+  public static ViewAction click(ViewAction rollbackAction) {
+    checkNotNull(rollbackAction);
+    return new GeneralClickAction(Tap.SINGLE, GeneralLocation.CENTER, Press.FINGER,
+        rollbackAction);
+  }
+
+  /**
+   * Returns an action that performs a swipe right-to-left across the vertical center of the
+   * view.<br>
+   * <br>
+   * View constraints:
+   * <ul>
+   * <li>must be displayed on screen
+   * <ul>
+   */
+  public static ViewAction swipeLeft() {
+    return new GeneralSwipeAction(Swipe.FAST, GeneralLocation.CENTER_RIGHT,
+        GeneralLocation.CENTER_LEFT, Press.FINGER);
+  }
+
+  /**
+   * Returns an action that performs a swipe left-to-right across the vertical center of the
+   * view.<br>
+   * <br>
+   * View constraints:
+   * <ul>
+   * <li>must be displayed on screen
+   * <ul>
+   */
+  public static ViewAction swipeRight() {
+    return new GeneralSwipeAction(Swipe.FAST, GeneralLocation.CENTER_LEFT,
+        GeneralLocation.CENTER_RIGHT, Press.FINGER);
+  }
+
+  /**
+   * Returns an action that closes soft keyboard. If the keyboard is already closed, it is a no-op.
+   */
+  public static ViewAction closeSoftKeyboard() {
+    return new CloseKeyboardAction();
+  }
+
+  /**
+   * Returns an action that presses the current action button (next, done, search, etc) on the IME
+   * (Input Method Editor). The selected view will have its onEditorAction method called.
+   */
+  public static ViewAction pressImeActionButton() {
+    return new EditorAction();
+  }
+
+  /**
+   * Returns an action that clicks the back button.
+   */
+  public static ViewAction pressBack() {
+    return pressKey(KeyEvent.KEYCODE_BACK);
+  }
+
+  /**
+   * Returns an action that presses the hardware menu key.
+   */
+  public static ViewAction pressMenuKey() {
+    return pressKey(KeyEvent.KEYCODE_MENU);
+  }
+
+  /**
+   * Returns an action that presses the key specified by the keyCode (eg. Keyevent.KEYCODE_BACK).
+   */
+  public static ViewAction pressKey(int keyCode) {
+    return new KeyEventAction(new EspressoKey.Builder().withKeyCode(keyCode).build());
+  }
+
+  /**
+   * Returns an action that presses the specified key with the specified modifiers.
+   */
+  public static ViewAction pressKey(EspressoKey key) {
+    return new KeyEventAction(key);
+  }
+
+  /**
+   * Returns an action that double clicks the view.<br>
+   * <br>
+   * View preconditions:
+   * <ul>
+   * <li>must be displayed on screen
+   * <ul>
+   */
+  public static ViewAction doubleClick() {
+    return new GeneralClickAction(Tap.DOUBLE, GeneralLocation.CENTER, Press.FINGER);
+  }
+
+  /**
+   * Returns an action that long clicks the view.<br>
+   *
+   * <br>
+   * View preconditions:
+   * <ul>
+   * <li>must be displayed on screen
+   * <ul>
+   */
+  public static ViewAction longClick() {
+    return new GeneralClickAction(Tap.LONG, GeneralLocation.CENTER, Press.FINGER);
+  }
+
+  /**
+   * Returns an action that scrolls to the view.<br>
+   * <br>
+   * View preconditions:
+   * <ul>
+   * <li>must be a descendant of ScrollView
+   * <li>must have visibility set to View.VISIBLE
+   * <ul>
+   */
+  public static ViewAction scrollTo() {
+    return new ScrollToAction();
+  }
+
+  /**
+   * Returns an action that types the provided string into the view.
+   * Appending a \n to the end of the string translates to a ENTER key event. Note: this method
+   * does not change cursor position in the focused view - text is inserted at the location where
+   * the cursor is currently pointed.<br>
+   * <br>
+   * View preconditions:
+   * <ul>
+   * <li>must be displayed on screen
+   * <li>must support input methods
+   * <li>must be already focused
+   * <ul>
+   */
+  public static ViewAction typeTextIntoFocusedView(String stringToBeTyped) {
+    return new TypeTextAction(stringToBeTyped, false /* tapToFocus */);
+  }
+
+  /**
+   * Returns an action that selects the view (by clicking on it) and types the provided string into
+   * the view. Appending a \n to the end of the string translates to a ENTER key event. Note: this
+   * method performs a tap on the view before typing to force the view into focus, if the view 
+   * already contains text this tap may place the cursor at an arbitrary position within the text.
+   * <br>
+   * <br>
+   * View preconditions:
+   * <ul>
+   * <li>must be displayed on screen
+   * <li>must support input methods
+   * <ul>
+   */
+  public static ViewAction typeText(String stringToBeTyped) {
+    return new TypeTextAction(stringToBeTyped);
+  }
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/assertion/ViewAssertions.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/assertion/ViewAssertions.java
new file mode 100644
index 0000000..eb14861
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/assertion/ViewAssertions.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.assertion;
+
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.assertThat;
+import static com.google.android.apps.common.testing.ui.espresso.util.TreeIterables.breadthFirstViewTraversal;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.hamcrest.Matchers.is;
+
+import com.google.android.apps.common.testing.ui.espresso.NoMatchingViewException;
+import com.google.android.apps.common.testing.ui.espresso.ViewAssertion;
+import com.google.android.apps.common.testing.ui.espresso.util.HumanReadables;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+
+import android.util.Log;
+import android.view.View;
+
+import junit.framework.AssertionFailedError;
+
+import org.hamcrest.Matcher;
+import org.hamcrest.StringDescription;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * A collection of common {@link ViewAssertion}s.
+ */
+public final class ViewAssertions {
+
+  private static final String TAG = ViewAssertions.class.getSimpleName();
+
+
+  private ViewAssertions() {}
+
+  /**
+   * Returns an assert that ensures the view matcher does not find any matching view in the
+   * hierarchy.
+   */
+  public static ViewAssertion doesNotExist() {
+    return new ViewAssertion() {
+      @Override
+      public void check(View view, NoMatchingViewException noView) {
+        if (view != null) {
+          assertThat("View is present in the hierarchy: " + HumanReadables.describe(view), true,
+              is(false));
+        }
+      }
+    };
+  }
+
+  /**
+   * Returns a generic {@link ViewAssertion} that asserts that a view exists in the view hierarchy
+   * and is matched by the given view matcher.
+   */
+  public static ViewAssertion matches(final Matcher<? super View> viewMatcher) {
+    checkNotNull(viewMatcher);
+    return new ViewAssertion() {
+      @Override
+      public void check(View view, NoMatchingViewException noViewException) {
+        StringDescription description = new StringDescription();
+        description.appendText("'");
+        viewMatcher.describeTo(description);
+        if (noViewException != null) {
+          description.appendText(String.format(
+              "' check could not be performed because view '%s' was not found.\n", viewMatcher));
+          Log.e(TAG, description.toString());
+          throw noViewException;
+        } else {
+          // TODO(valeraz): ideally, we should append the matcher used to find the view
+          // This can be done in DefaultFailureHandler (just like we currently to with
+          // PerformException)
+          description.appendText("' doesn't match the selected view.");
+          assertThat(description.toString(), view, viewMatcher);
+        }
+      }
+    };
+  }
+
+
+  /**
+   * Returns a generic {@link ViewAssertion} that asserts that the descendant views selected by the
+   * selector match the specified matcher.
+   *
+   *  Example: onView(rootView).check(selectedDescendantsMatch(
+   * not(isAssignableFrom(TextView.class)), hasContentDescription()));
+   */
+  public static ViewAssertion selectedDescendantsMatch(
+      final Matcher<View> selector, final Matcher<View> matcher) {
+    return new ViewAssertion() {
+      @SuppressWarnings("unchecked")
+      @Override
+      public void check(View view, NoMatchingViewException noViewException) {
+        Preconditions.checkNotNull(view);
+
+        final Predicate<View> viewPredicate = new Predicate<View>() {
+          @Override
+          public boolean apply(View input) {
+            return selector.matches(input);
+          }
+        };
+
+        Iterator<View> selectedViewIterator =
+            Iterables.filter(breadthFirstViewTraversal(view), viewPredicate).iterator();
+
+        List<View> nonMatchingViews = new ArrayList<View>();
+        while (selectedViewIterator.hasNext()) {
+          View selectedView = selectedViewIterator.next();
+
+          if (!matcher.matches(selectedView)) {
+            nonMatchingViews.add(selectedView);
+          }
+        }
+
+        if (nonMatchingViews.size() > 0) {
+          String errorMessage = HumanReadables.getViewHierarchyErrorMessage(view,
+              nonMatchingViews,
+              String.format("At least one view did not match the required matcher: %s", matcher),
+              "****DOES NOT MATCH****");
+          throw new AssertionFailedError(errorMessage);
+        }
+      }
+    };
+  }
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/AsyncTaskPoolMonitor.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/AsyncTaskPoolMonitor.java
new file mode 100644
index 0000000..082c045
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/AsyncTaskPoolMonitor.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.base;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+import java.util.concurrent.BrokenBarrierException;
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Provides a way to monitor AsyncTask's work queue to ensure that there is no work pending
+ * or executing (and to allow notification of idleness).
+ *
+ * This class is based on the assumption that we can get at the ThreadPoolExecutor AsyncTask uses.
+ * That is currently possible and easy in Froyo to JB. If it ever becomes impossible, as long as we
+ * know the max # of executor threads the AsyncTask framework allows we can still use this
+ * interface, just need a different implementation.
+ */
+class AsyncTaskPoolMonitor {
+  private final AtomicReference<IdleMonitor> monitor = new AtomicReference<IdleMonitor>(null);
+  private final ThreadPoolExecutor pool;
+  private final AtomicInteger activeBarrierChecks = new AtomicInteger(0);
+
+  AsyncTaskPoolMonitor(ThreadPoolExecutor pool) {
+    this.pool = checkNotNull(pool);
+  }
+
+  /**
+   * Checks if the pool is idle at this moment.
+   *
+   * @return true if the pool is idle, false otherwise.
+   */
+  boolean isIdleNow() {
+    if (!pool.getQueue().isEmpty()) {
+      return false;
+    } else {
+      int activeCount = pool.getActiveCount();
+      if (0 != activeCount) {
+        if (monitor.get() == null) {
+          // if there's no idle monitor scheduled and there are still barrier
+          // checks running, they are about to exit, ignore them.
+          activeCount = activeCount - activeBarrierChecks.get();
+        }
+      }
+      return 0 == activeCount;
+    }
+  }
+
+  /**
+   * Notifies caller once the pool is idle.
+   *
+   * We check for idle-ness by submitting the max # of tasks the pool will take and blocking
+   * the tasks until they are all executing. Then we know there are no other tasks _currently_
+   * executing in the pool, we look back at the work queue to see if its backed up, if it is
+   * we reenqueue ourselves and try again.
+   *
+   * Obviously this strategy will fail horribly if 2 parties are doing it at the same time,
+   * we prevent recursion here the best we can.
+   *
+   * @param idleCallback called once the pool is idle.
+   */
+  void notifyWhenIdle(final Runnable idleCallback) {
+    checkNotNull(idleCallback);
+    IdleMonitor myMonitor = new IdleMonitor(idleCallback);
+    checkState(monitor.compareAndSet(null, myMonitor), "cannot monitor for idle recursively!");
+    myMonitor.monitorForIdle();
+  }
+
+  /**
+   * Stops the idle monitoring mechanism if it is in place.
+   *
+   * Note: the callback may still be invoked after this method is called. The only thing
+   * this method guarantees is that we will stop/cancel any blockign tasks we've placed
+   * on the thread pool.
+   */
+  void cancelIdleMonitor() {
+    IdleMonitor myMonitor = monitor.getAndSet(null);
+    if (null != myMonitor) {
+      myMonitor.poison();
+    }
+  }
+
+  private class IdleMonitor {
+    private final Runnable onIdle;
+    private final AtomicInteger barrierGeneration = new AtomicInteger(0);
+    private final CyclicBarrier barrier;
+    // written by main, read by all.
+    private volatile boolean poisoned;
+
+    private IdleMonitor(final Runnable onIdle) {
+      this.onIdle = checkNotNull(onIdle);
+      this.barrier = new CyclicBarrier(pool.getCorePoolSize(),
+          new Runnable() {
+            @Override
+            public void run() {
+              if (pool.getQueue().isEmpty()) {
+                // no one is behind us, so the queue is idle!
+                monitor.compareAndSet(IdleMonitor.this, null);
+                onIdle.run();
+              } else {
+                // work is waiting behind us, enqueue another block of tasks and
+                // hopefully when they're all running, the queue will be empty.
+                monitorForIdle();
+              }
+
+            }
+          });
+    }
+
+    /**
+     * Stops this monitor from using the thread pool's resources, it may still cause the
+     * callback to be executed though.
+     */
+    private void poison() {
+      poisoned = true;
+      barrier.reset();
+    }
+
+    private void monitorForIdle() {
+      if (poisoned) {
+        return;
+      }
+
+      if (isIdleNow()) {
+        monitor.compareAndSet(this, null);
+        onIdle.run();
+      } else {
+        // Submit N tasks that will block until they are all running on the thread pool.
+        // at this point we can check the pool's queue and verify that there are no new
+        // tasks behind us and deem the queue idle.
+
+        int poolSize = pool.getCorePoolSize();
+        final BarrierRestarter restarter = new BarrierRestarter(barrier, barrierGeneration);
+
+        for (int i = 0; i < poolSize; i++) {
+          pool.execute(new Runnable() {
+            @Override
+            public void run() {
+              while (!poisoned) {
+                activeBarrierChecks.incrementAndGet();
+                int myGeneration = barrierGeneration.get();
+                try {
+                  barrier.await();
+                  return;
+                } catch (InterruptedException ie) {
+                  // sorry - I cant let you interrupt me!
+                  restarter.restart(myGeneration);
+                } catch (BrokenBarrierException bbe) {
+                  restarter.restart(myGeneration);
+                } finally {
+                  activeBarrierChecks.decrementAndGet();
+                }
+              }
+            }
+          });
+        }
+      }
+    }
+  }
+
+
+  private static class BarrierRestarter {
+    private final CyclicBarrier barrier;
+    private final AtomicInteger barrierGeneration;
+    BarrierRestarter(CyclicBarrier barrier, AtomicInteger barrierGeneration) {
+      this.barrier = barrier;
+      this.barrierGeneration = barrierGeneration;
+    }
+
+    /**
+     * restarts the barrier.
+     *
+     * After the calling this function it is guaranteed that barrier generation has been incremented
+     * and the barrier can be awaited on again.
+     *
+     * @param fromGeneration the generation that encountered the breaking exception.
+     */
+    synchronized void restart(int fromGeneration) {
+      // must be synchronized. T1 could pass the if check, be suspended before calling reset, T2
+      // sails thru - and awaits on the barrier again before T1 has awoken and reset it.
+      int nextGen = fromGeneration + 1;
+      if (barrierGeneration.compareAndSet(fromGeneration, nextGen)) {
+        // first time we've seen fromGeneration request a reset. lets reset the barrier.
+        barrier.reset();
+      } else {
+        // some other thread has already reset the barrier - this request is a no op.
+      }
+    }
+  }
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/BaseLayerModule.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/BaseLayerModule.java
new file mode 100644
index 0000000..6615b1d
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/BaseLayerModule.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.base;
+
+import com.google.android.apps.common.testing.testrunner.ActivityLifecycleMonitor;
+import com.google.android.apps.common.testing.testrunner.ActivityLifecycleMonitorRegistry;
+import com.google.android.apps.common.testing.testrunner.InstrumentationRegistry;
+import com.google.android.apps.common.testing.testrunner.inject.TargetContext;
+import com.google.android.apps.common.testing.ui.espresso.FailureHandler;
+import com.google.android.apps.common.testing.ui.espresso.Root;
+import com.google.android.apps.common.testing.ui.espresso.UiController;
+import com.google.common.base.Optional;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+
+import dagger.Module;
+import dagger.Provides;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+/**
+ * Dagger module for creating the implementation classes within the base package.
+ */
+@Module(library = true, injects = {
+    BaseLayerModule.FailureHandlerHolder.class, FailureHandler.class})
+public class BaseLayerModule {
+
+  @Provides @Singleton
+  public ActivityLifecycleMonitor provideLifecycleMonitor() {
+    // TODO(user): replace with installation of AndroidInstrumentationModule once
+    // proguard issues resolved.
+    return ActivityLifecycleMonitorRegistry.getInstance();
+  }
+
+  @Provides @TargetContext
+  public Context provideTargetContext() {
+    // TODO(user): replace with installation of AndroidInstrumentationModule once
+    // proguard issues resolved.
+    return InstrumentationRegistry.getInstance().getTargetContext();
+  }
+
+  @Provides @Singleton
+  public Looper provideMainLooper() {
+    return Looper.getMainLooper();
+  }
+
+  @Provides
+  public UiController provideUiController(UiControllerImpl uiControllerImpl) {
+    return uiControllerImpl;
+  }
+
+  @Provides @Singleton @CompatAsyncTask
+  public Optional<AsyncTaskPoolMonitor> provideCompatAsyncTaskMonitor(
+      ThreadPoolExecutorExtractor extractor) {
+    Optional<ThreadPoolExecutor> compatThreadPool = extractor.getCompatAsyncTaskThreadPool();
+    if (compatThreadPool.isPresent()) {
+      return Optional.of(new AsyncTaskPoolMonitor(compatThreadPool.get()));
+    } else {
+      return Optional.<AsyncTaskPoolMonitor>absent();
+    }
+  }
+
+  @Provides @Singleton @MainThread
+  public Executor provideMainThreadExecutor(Looper mainLooper) {
+    final Handler handler = new Handler(mainLooper);
+    return new Executor() {
+      @Override
+      public void execute(Runnable runnable) {
+        handler.post(runnable);
+      }
+    };
+  }
+
+  @Provides @Singleton @SdkAsyncTask
+  public AsyncTaskPoolMonitor provideSdkAsyncTaskMonitor(ThreadPoolExecutorExtractor extractor) {
+    return new AsyncTaskPoolMonitor(extractor.getAsyncTaskThreadPool());
+
+  }
+
+  @Provides
+  public List<Root> provideKnownRoots(RootsOracle rootsOracle) {
+    // RootsOracle acts as a provider, but returning Providers is illegal, so delegate.
+    return rootsOracle.get();
+  }
+
+  @Provides @Singleton
+  public EventInjector provideEventInjector() {
+    // On API 16 and above, android uses input manager to inject events. On API < 16,
+    // they use Window Manager. So we need to create our InjectionStrategy depending on the api
+    // level. Instrumentation does not check if the event presses went through by checking the
+    // boolean return value of injectInputEvent, which is why we created this class to better
+    // handle lost/dropped press events. Instrumentation cannot be used as a fallback strategy,
+    // since this will be executed on the main thread.
+    int sdkVersion = Build.VERSION.SDK_INT;
+    EventInjectionStrategy injectionStrategy = null;
+    if (sdkVersion >= 16) { // Use InputManager for API level 16 and up.
+      InputManagerEventInjectionStrategy strategy = new InputManagerEventInjectionStrategy();
+      strategy.initialize();
+      injectionStrategy = strategy;
+    } else if (sdkVersion >= 7) {
+      // else Use WindowManager for API level 15 through 7.
+      WindowManagerEventInjectionStrategy strategy = new WindowManagerEventInjectionStrategy();
+      strategy.initialize();
+      injectionStrategy = strategy;
+    } else {
+      throw new RuntimeException(
+          "API Level 6 and below is not supported. You are running: " + sdkVersion);
+    }
+    return new EventInjector(injectionStrategy);
+  }
+
+  /**
+   * Holder for AtomicReference<FailureHandler> which allows updating it at runtime.
+   */
+  @Singleton
+  public static class FailureHandlerHolder {
+    private final AtomicReference<FailureHandler> holder;
+
+    @Inject
+    public FailureHandlerHolder(@Default FailureHandler defaultHandler) {
+      holder = new AtomicReference<FailureHandler>(defaultHandler);
+    }
+
+    public void update(FailureHandler handler) {
+      holder.set(handler);
+    }
+
+    public FailureHandler get() {
+      return holder.get();
+    }
+  }
+
+  @Provides
+  FailureHandler provideFailureHandler(FailureHandlerHolder holder) {
+    return holder.get();
+  }
+
+  @Provides
+  @Default
+  FailureHandler provideFailureHander(DefaultFailureHandler impl) {
+    return impl;
+  }
+
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/CompatAsyncTask.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/CompatAsyncTask.java
new file mode 100644
index 0000000..11ec6ab
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/CompatAsyncTask.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.base;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import javax.inject.Qualifier;
+
+/**
+ * Annotates a AsyncTaskMonitor as monitoring the CompatAsyncTask pool
+ */
+@Qualifier
+@Retention(RetentionPolicy.RUNTIME)
+@interface CompatAsyncTask { }
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/Default.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/Default.java
new file mode 100644
index 0000000..b54440d
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/Default.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.base;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import javax.inject.Qualifier;
+
+/**
+ * Annotates a default provider.
+ */
+@Qualifier
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Default {
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/DefaultFailureHandler.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/DefaultFailureHandler.java
new file mode 100644
index 0000000..b1e43da
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/DefaultFailureHandler.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.base;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Throwables.propagate;
+
+import com.google.android.apps.common.testing.testrunner.inject.TargetContext;
+import com.google.android.apps.common.testing.ui.espresso.EspressoException;
+import com.google.android.apps.common.testing.ui.espresso.FailureHandler;
+import com.google.android.apps.common.testing.ui.espresso.PerformException;
+
+import android.content.Context;
+import android.view.View;
+
+import junit.framework.AssertionFailedError;
+
+import org.hamcrest.Matcher;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.inject.Inject;
+
+/**
+ * Espresso's default {@link FailureHandler}. If this does not fit your needs, feel free to provide
+ * your own implementation via Espresso.setFailureHandler(FailureHandler).
+ */
+public final class DefaultFailureHandler implements FailureHandler {
+
+  private static final AtomicInteger failureCount = new AtomicInteger(0);
+  private final Context appContext;
+
+  @Inject
+  public DefaultFailureHandler(@TargetContext Context appContext) {
+    this.appContext = checkNotNull(appContext);
+  }
+
+  @Override
+  public void handle(Throwable error, Matcher<View> viewMatcher) {
+    if (error instanceof EspressoException || error instanceof AssertionFailedError
+        || error instanceof AssertionError) {
+      throw propagate(getUserFriendlyError(error, viewMatcher));
+    } else {
+      throw propagate(error);
+    }
+  }
+
+  /**
+   * When the error is coming from espresso, it is more user friendly to:
+   * 1. propagate assertions as assertions
+   * 2. swap the stack trace of the error to that of current thread (which will show
+   * directly where the actual problem is)
+   */
+  private Throwable getUserFriendlyError(Throwable error, Matcher<View> viewMatcher) {
+    if (error instanceof PerformException) {
+      // Re-throw the exception with the viewMatcher (used to locate the view) as the view
+      // description (makes the error more readable). The reason we do this here: not all creators
+      // of PerformException have access to the viewMatcher.
+      throw new PerformException.Builder()
+        .from((PerformException) error)
+        .withViewDescription(viewMatcher.toString())
+        .build();
+    }
+
+    if (error instanceof AssertionError) {
+      // reports Failure instead of Error.
+      // assertThat(...) throws an AssertionFailedError.
+      error = new AssertionFailedWithCauseError(error.getMessage(), error);
+    }
+
+    error.setStackTrace(Thread.currentThread().getStackTrace());
+    return error;
+  }
+
+  private static final class AssertionFailedWithCauseError extends AssertionFailedError {
+    /* junit hides the cause constructor. */
+    public AssertionFailedWithCauseError(String message, Throwable cause) {
+      super(message);
+      initCause(cause);
+    }
+  }
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/EventInjectionStrategy.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/EventInjectionStrategy.java
new file mode 100644
index 0000000..3378197
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/EventInjectionStrategy.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.base;
+
+import com.google.android.apps.common.testing.ui.espresso.InjectEventSecurityException;
+
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+/**
+ * Injects Events into the application under test. Implementors should expect to be called
+ * from the UI thread and are responsible for ensuring the event gets delivered or indicating that
+ * it could not be delivered.
+ */
+interface EventInjectionStrategy {
+  /**
+   * Injects the given {@link KeyEvent} into the android system.
+   *
+   * @param keyEvent The event to inject
+   * @return {@code true} if the input was inject successfully, {@code false} otherwise.
+   * @throws InjectEventSecurityException if the MotionEvent would be delivered to an area of the
+   *         screen that is not owned by the application under test.
+   */
+  boolean injectKeyEvent(KeyEvent keyEvent) throws InjectEventSecurityException;
+
+  /**
+   * Injects the given {@link MotionEvent} into the android system.
+   *
+   * @param motionEvent The event to inject
+   * @return {@code true} if the input was inject successfully, {@code false} otherwise.
+   * @throws InjectEventSecurityException if the MotionEvent would be delivered to an area of the
+   *         screen that is not owned by the application under test.
+   */
+  boolean injectMotionEvent(MotionEvent motionEvent) throws InjectEventSecurityException;
+
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/EventInjector.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/EventInjector.java
new file mode 100644
index 0000000..0728331
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/EventInjector.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.base;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.android.apps.common.testing.ui.espresso.InjectEventSecurityException;
+
+import android.os.Build;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+/**
+ * Responsible for selecting the proper strategy for injecting MotionEvents to the application under
+ * test.
+ */
+final class EventInjector {
+  private static final String TAG = EventInjector.class.getSimpleName();
+  private final EventInjectionStrategy injectionStrategy;
+
+  EventInjector(EventInjectionStrategy injectionStrategy) {
+    this.injectionStrategy = checkNotNull(injectionStrategy);
+  }
+
+  boolean injectKeyEvent(KeyEvent event) throws InjectEventSecurityException {
+    long downTime = event.getDownTime();
+    long eventTime = event.getEventTime();
+    int action = event.getAction();
+    int code = event.getKeyCode();
+    int repeatCount = event.getRepeatCount();
+    int metaState = event.getMetaState();
+    int deviceId = event.getDeviceId();
+    int scancode = event.getScanCode();
+    int flags = event.getFlags();
+
+    if (eventTime == 0) {
+      eventTime = SystemClock.uptimeMillis();
+    }
+
+    if (downTime == 0) {
+      downTime = eventTime;
+    }
+
+    // API < 9 does not have constructor with source (nor has source field).
+    KeyEvent newEvent;
+    if (Build.VERSION.SDK_INT < 9) {
+      newEvent = new KeyEvent(downTime,
+          eventTime,
+          action,
+          code,
+          repeatCount,
+          metaState,
+          deviceId,
+          scancode,
+          flags | KeyEvent.FLAG_FROM_SYSTEM);
+    } else {
+      int source = event.getSource();
+      newEvent = new KeyEvent(downTime,
+          eventTime,
+          action,
+          code,
+          repeatCount,
+          metaState,
+          deviceId,
+          scancode,
+          flags | KeyEvent.FLAG_FROM_SYSTEM,
+          source);
+    }
+
+    Log.v(
+        "ESP_TRACE",
+        String.format(
+            "%s:Injecting event for character (%c) with key code (%s) downtime: (%s)", TAG,
+            newEvent.getUnicodeChar(), newEvent.getKeyCode(), newEvent.getDownTime()));
+
+    return injectionStrategy.injectKeyEvent(newEvent);
+  }
+
+  boolean injectMotionEvent(MotionEvent event) throws InjectEventSecurityException {
+    return injectionStrategy.injectMotionEvent(event);
+  }
+
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/IdlingResourceRegistry.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/IdlingResourceRegistry.java
new file mode 100644
index 0000000..e390f0f
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/IdlingResourceRegistry.java
@@ -0,0 +1,289 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.base;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.android.apps.common.testing.ui.espresso.IdlingPolicies;
+import com.google.android.apps.common.testing.ui.espresso.IdlingPolicy;
+import com.google.android.apps.common.testing.ui.espresso.IdlingResource;
+import com.google.android.apps.common.testing.ui.espresso.IdlingResource.ResourceCallback;
+import com.google.common.collect.Lists;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+
+import java.util.BitSet;
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+/**
+ * Keeps track of user-registered {@link IdlingResource}s.
+ */
+@Singleton
+public final class IdlingResourceRegistry {
+  private static final String TAG = IdlingResourceRegistry.class.getSimpleName();
+
+  private static final int DYNAMIC_RESOURCE_HAS_IDLED = 1;
+  private static final int TIMEOUT_OCCURRED = 2;
+  private static final int IDLE_WARNING_REACHED = 3;
+  private static final int POSSIBLE_RACE_CONDITION_DETECTED = 4;
+  private static final Object TIMEOUT_MESSAGE_TAG = new Object();
+
+  private static final IdleNotificationCallback NO_OP_CALLBACK = new IdleNotificationCallback() {
+
+    @Override
+    public void allResourcesIdle() {}
+
+    @Override
+    public void resourcesStillBusyWarning(List<String> busys) {}
+
+    @Override
+    public void resourcesHaveTimedOut(List<String> busys) {}
+  };
+
+  // resources and idleState should only be accessed on main thread
+  private final List<IdlingResource> resources = Lists.newArrayList();
+  // idleState.get(i) == true indicates resources.get(i) is idle, false indicates it's busy
+  private final BitSet idleState = new BitSet();
+  private final Looper looper;
+  private final Handler handler;
+  private final Dispatcher dispatcher;
+  private IdleNotificationCallback idleNotificationCallback = NO_OP_CALLBACK;
+
+  @Inject
+  public IdlingResourceRegistry(Looper looper) {
+    this.looper = looper;
+    this.dispatcher = new Dispatcher();
+    this.handler = new Handler(looper, dispatcher);
+  }
+
+  /**
+   * Registers the given resource.
+   */
+  public void register(final IdlingResource resource) {
+    checkNotNull(resource);
+    if (Looper.myLooper() != looper) {
+      handler.post(new Runnable() {
+        @Override
+        public void run() {
+          register(resource);
+        }
+      });
+    } else {      
+      for (IdlingResource oldResource : resources) {
+        if (resource.getName().equals(oldResource.getName())) {
+          // This does not throw an error to avoid leaving tests that register resource in test
+          // setup in an undeterministic state (we cannot assume that everyone clears vm state
+          // between each test run)
+          Log.e(TAG, String.format("Attempted to register resource with same names:" +
+              " %s. R1: %s R2: %s.\nDuplicate resource registration will be ignored.",
+              resource.getName(), resource, oldResource));
+          return;
+        }
+      }
+      resources.add(resource);
+      final int position = resources.size() - 1;
+      registerToIdleCallback(resource, position);
+      idleState.set(position, resource.isIdleNow());
+    }
+  }
+
+  public void registerLooper(Looper looper, boolean considerWaitIdle) {
+    checkNotNull(looper);
+    checkArgument(Looper.getMainLooper() != looper, "Not intended for use with main looper!");
+    register(new LooperIdlingResource(looper, considerWaitIdle));
+  }
+
+  private void registerToIdleCallback(IdlingResource resource, final int position) {
+    resource.registerIdleTransitionCallback(new ResourceCallback() {
+      @Override
+      public void onTransitionToIdle() {
+        Message m = handler.obtainMessage(DYNAMIC_RESOURCE_HAS_IDLED);
+        m.arg1 = position;
+        handler.sendMessage(m);
+      }
+    });
+  }
+
+  boolean allResourcesAreIdle() {
+    checkState(Looper.myLooper() == looper);
+    for (int i = idleState.nextSetBit(0); i >= 0 && i < resources.size();
+        i = idleState.nextSetBit(i + 1)) {
+      idleState.set(i, resources.get(i).isIdleNow());
+    }
+    return idleState.cardinality() == resources.size();
+  }
+
+  interface IdleNotificationCallback {
+    public void allResourcesIdle();
+
+    public void resourcesStillBusyWarning(List<String> busyResourceNames);
+
+    public void resourcesHaveTimedOut(List<String> busyResourceNames);
+  }
+
+  void notifyWhenAllResourcesAreIdle(IdleNotificationCallback callback) {
+    checkNotNull(callback);
+    checkState(Looper.myLooper() == looper);
+    checkState(idleNotificationCallback == NO_OP_CALLBACK, "Callback has already been registered.");
+    if (allResourcesAreIdle()) {
+      callback.allResourcesIdle();
+    } else {
+      idleNotificationCallback = callback;
+      scheduleTimeoutMessages();
+    }
+  }
+
+  void cancelIdleMonitor() {
+    dispatcher.deregister();
+  }
+
+  private void scheduleTimeoutMessages() {
+    IdlingPolicy warning = IdlingPolicies.getDynamicIdlingResourceWarningPolicy();
+    Message timeoutWarning = handler.obtainMessage(IDLE_WARNING_REACHED, TIMEOUT_MESSAGE_TAG);
+    handler.sendMessageDelayed(timeoutWarning, warning.getIdleTimeoutUnit().toMillis(
+        warning.getIdleTimeout()));
+    Message timeoutError = handler.obtainMessage(TIMEOUT_OCCURRED, TIMEOUT_MESSAGE_TAG);
+    IdlingPolicy error = IdlingPolicies.getDynamicIdlingResourceErrorPolicy();
+
+    handler.sendMessageDelayed(timeoutError, error.getIdleTimeoutUnit().toMillis(
+        error.getIdleTimeout()));
+  }
+
+  private List<String> getBusyResources() {
+    List<String> busyResourceNames = Lists.newArrayList();
+    List<Integer> racyResources = Lists.newArrayList();
+
+    for (int i = 0; i < resources.size(); i++) {
+      IdlingResource resource = resources.get(i);
+      if (!idleState.get(i)) {
+        if (resource.isIdleNow()) {
+          // We have not been notified of a BUSY -> IDLE transition, but the resource is telling us
+          // its that its idle. Either it's a race condition or is this resource buggy.
+          racyResources.add(i);
+        } else {
+          busyResourceNames.add(resource.getName());
+        }
+      }
+    }
+
+    if (!racyResources.isEmpty()) {
+      Message raceBuster = handler.obtainMessage(POSSIBLE_RACE_CONDITION_DETECTED,
+          TIMEOUT_MESSAGE_TAG);
+      raceBuster.obj = racyResources;
+      handler.sendMessage(raceBuster);
+      return null;
+    } else {
+      return busyResourceNames;
+    }
+  }
+
+
+  private class Dispatcher implements Handler.Callback {
+    @Override
+    public boolean handleMessage(Message m) {
+      switch (m.what) {
+        case DYNAMIC_RESOURCE_HAS_IDLED:
+          handleResourceIdled(m);
+          break;
+        case IDLE_WARNING_REACHED:
+          handleTimeoutWarning();
+          break;
+        case TIMEOUT_OCCURRED:
+          handleTimeout();
+          break;
+        case POSSIBLE_RACE_CONDITION_DETECTED:
+          handleRaceCondition(m);
+          break;
+        default:
+          Log.w(TAG, "Unknown message type: " + m);
+          return false;
+      }
+      return true;
+    }
+
+    private void handleResourceIdled(Message m) {
+      idleState.set(m.arg1, true);
+      if (idleState.cardinality() == resources.size()) {
+        try {
+          idleNotificationCallback.allResourcesIdle();
+        } finally {
+          deregister();
+        }
+      }
+    }
+
+    private void handleTimeoutWarning() {
+      List<String> busyResources = getBusyResources();
+      if (busyResources == null) {
+        // null indicates that there is either a race or a programming error
+        // a race detector message has been inserted into the q.
+        // reinsert the idle_warning_reached message into the q directly after it
+        // so we generate warnings if the system is still sane.
+        handler.sendMessage(handler.obtainMessage(IDLE_WARNING_REACHED, TIMEOUT_MESSAGE_TAG));
+      } else {
+        IdlingPolicy warning = IdlingPolicies.getDynamicIdlingResourceWarningPolicy();
+        idleNotificationCallback.resourcesStillBusyWarning(busyResources);
+        handler.sendMessageDelayed(
+            handler.obtainMessage(IDLE_WARNING_REACHED, TIMEOUT_MESSAGE_TAG),
+            warning.getIdleTimeoutUnit().toMillis(warning.getIdleTimeout()));
+      }
+    }
+
+    private void handleTimeout() {
+      List<String> busyResources = getBusyResources();
+      if (busyResources == null) {
+        // detected a possible race... we've enqueued a race busting message
+        // so either that'll resolve the race or kill the app because it's buggy.
+        // if the race resolves, we need to timeout properly.
+        handler.sendMessage(handler.obtainMessage(TIMEOUT_OCCURRED, TIMEOUT_MESSAGE_TAG));
+      } else {
+        try {
+          idleNotificationCallback.resourcesHaveTimedOut(busyResources);
+        } finally {
+          deregister();
+        }
+      }
+    }
+
+    @SuppressWarnings("unchecked")
+    private void handleRaceCondition(Message m) {
+      for (Integer i : (List<Integer>) m.obj) {
+        if (idleState.get(i)) {
+          // it was a race... i is now idle, everything is fine...
+        } else {
+          throw new IllegalStateException(String.format(
+              "Resource %s isIdleNow() is returning true, but a message indicating that the "
+              + "resource has transitioned from busy to idle was never sent.",
+              resources.get(i).getName()));
+        }
+      }
+    }
+
+    private void deregister() {
+      handler.removeCallbacksAndMessages(TIMEOUT_MESSAGE_TAG);
+      idleNotificationCallback = NO_OP_CALLBACK;
+    }
+  }
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/InputManagerEventInjectionStrategy.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/InputManagerEventInjectionStrategy.java
new file mode 100644
index 0000000..d324795
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/InputManagerEventInjectionStrategy.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.base;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.base.Throwables.propagate;
+
+import com.google.android.apps.common.testing.ui.espresso.InjectEventSecurityException;
+
+import android.os.Build;
+import android.util.Log;
+import android.view.InputDevice;
+import android.view.InputEvent;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * An {@link EventInjectionStrategy} that uses the input manager to inject Events.
+ * This strategy supports API level 16 and above.
+ */
+final class InputManagerEventInjectionStrategy implements EventInjectionStrategy {
+  private static final String TAG = InputManagerEventInjectionStrategy.class.getSimpleName();
+
+  // Used in reflection
+  private boolean initComplete;
+  private Method injectInputEventMethod;
+  private Method setSourceMotionMethod;
+  private Object instanceInputManagerObject;
+  private int motionEventMode;
+  private int keyEventMode;
+
+  InputManagerEventInjectionStrategy() {
+    checkState(Build.VERSION.SDK_INT >= 16, "Unsupported API level.");
+  }
+
+  void initialize() {
+    if (initComplete) {
+      return;
+    }
+
+    try {
+      Log.d(TAG, "Creating injection strategy with input manager.");
+
+      // Get the InputputManager class object and initialize if necessary.
+      Class<?> inputManagerClassObject = Class.forName("android.hardware.input.InputManager");
+      Method getInstanceMethod = inputManagerClassObject.getDeclaredMethod("getInstance");
+      getInstanceMethod.setAccessible(true);
+
+      instanceInputManagerObject = getInstanceMethod.invoke(inputManagerClassObject);
+
+      injectInputEventMethod = instanceInputManagerObject.getClass()
+          .getDeclaredMethod("injectInputEvent", InputEvent.class, Integer.TYPE);
+      injectInputEventMethod.setAccessible(true);
+
+      // Setting event mode to INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH to ensure
+      // that we've dispatched the event and any side effects its had on the view hierarchy
+      // have occurred.
+      Field motionEventModeField =
+          inputManagerClassObject.getField("INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH");
+      motionEventModeField.setAccessible(true);
+      motionEventMode = motionEventModeField.getInt(inputManagerClassObject);
+
+      Field keyEventModeField =
+          inputManagerClassObject.getField("INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH");
+      keyEventModeField.setAccessible(true);
+      keyEventMode = keyEventModeField.getInt(inputManagerClassObject);
+
+      setSourceMotionMethod = MotionEvent.class.getDeclaredMethod("setSource", Integer.TYPE);
+      InputEvent.class.getDeclaredMethod("getSequenceNumber");
+      initComplete = true;
+    } catch (ClassNotFoundException e) {
+      propagate(e);
+    } catch (IllegalAccessException e) {
+      propagate(e);
+    } catch (IllegalArgumentException e) {
+      propagate(e);
+    } catch (InvocationTargetException e) {
+      propagate(e);
+    } catch (NoSuchMethodException e) {
+      propagate(e);
+    } catch (SecurityException e) {
+      propagate(e);
+    } catch (NoSuchFieldException e) {
+      propagate(e);
+    }
+  }
+
+  @Override
+  public boolean injectKeyEvent(KeyEvent keyEvent) throws InjectEventSecurityException {
+    try {
+       return (Boolean) injectInputEventMethod.invoke(instanceInputManagerObject,
+           keyEvent, keyEventMode);
+    } catch (IllegalAccessException e) {
+      propagate(e);
+    } catch (IllegalArgumentException e) {
+      propagate(e);
+    } catch (InvocationTargetException e) {
+      Throwable cause = e.getCause();
+      if (cause instanceof SecurityException) {
+        throw new InjectEventSecurityException(cause);
+      }
+      propagate(e);
+    } catch (SecurityException e) {
+      throw new InjectEventSecurityException(e);
+    }
+    return false;
+  }
+
+  @Override
+  public boolean injectMotionEvent(MotionEvent motionEvent) throws InjectEventSecurityException {
+    try {
+      // Need to set the event source to touch screen, otherwise the input can be ignored even
+      // though injecting it would be successful.
+      // TODO(user): proper handling of events from a trackball (SOURCE_TRACKBALL) and joystick.
+      if ((motionEvent.getSource() & InputDevice.SOURCE_CLASS_POINTER) == 0
+          && !isFromTouchpadInGlassDevice(motionEvent)) {
+        // Need to do runtime invocation of setSource because it was not added until 2.3_r1.
+        setSourceMotionMethod.invoke(motionEvent, InputDevice.SOURCE_TOUCHSCREEN);
+      }
+      return (Boolean) injectInputEventMethod.invoke(instanceInputManagerObject,
+          motionEvent, motionEventMode);
+    } catch (IllegalAccessException e) {
+      propagate(e);
+    } catch (IllegalArgumentException e) {
+      propagate(e);
+    } catch (InvocationTargetException e) {
+      Throwable cause = e.getCause();
+      if (cause instanceof SecurityException) {
+        throw new InjectEventSecurityException(cause);
+      }
+      propagate(e);
+    } catch (SecurityException e) {
+      throw new InjectEventSecurityException(e);
+    }
+    return false;
+  }
+
+  // We'd like to inject non-pointer events sourced from touchpad in Glass.
+  private static boolean isFromTouchpadInGlassDevice(MotionEvent motionEvent) {
+    return (Build.DEVICE.contains("glass")
+        || Build.DEVICE.contains("Glass") || Build.DEVICE.contains("wingman"))
+        && ((motionEvent.getSource() & InputDevice.SOURCE_TOUCHPAD) != 0);
+  }
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/LooperIdlingResource.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/LooperIdlingResource.java
new file mode 100644
index 0000000..b75fd36
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/LooperIdlingResource.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.base;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.android.apps.common.testing.ui.espresso.IdlingResource;
+import com.google.android.apps.common.testing.ui.espresso.IdlingResource.ResourceCallback;
+import com.google.android.apps.common.testing.ui.espresso.base.QueueInterrogator.QueueState;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.MessageQueue.IdleHandler;
+
+/**
+ * An Idling Resource Adapter for Loopers.
+ */
+final class LooperIdlingResource implements IdlingResource {
+
+  private static final String TAG = "LooperIdleResource";
+
+  private final boolean considerWaitIdle;
+  private final Looper monitoredLooper;
+  private final Handler monitoredHandler;
+
+  private ResourceCallback resourceCallback;
+
+  LooperIdlingResource(Looper monitoredLooper, boolean considerWaitIdle) {
+    this.monitoredLooper = checkNotNull(monitoredLooper);
+    this.monitoredHandler = new Handler(monitoredLooper);
+    this.considerWaitIdle = considerWaitIdle;
+    checkState(Looper.getMainLooper() != monitoredLooper, "Not for use with main looper.");
+  }
+
+  // Only assigned and read from the main loop.
+  private QueueInterrogator queueInterrogator;
+
+  @Override
+  public String getName() {
+    return monitoredLooper.getThread().getName();
+  }
+
+  @Override
+  public boolean isIdleNow() {
+    // on main thread here.
+    QueueState state = queueInterrogator.determineQueueState();
+    boolean idle = state == QueueState.EMPTY || state == QueueState.TASK_DUE_LONG;
+    boolean idleWait = considerWaitIdle
+        && monitoredLooper.getThread().getState() == Thread.State.WAITING;
+    if (idleWait) {
+      if (resourceCallback != null) {
+        resourceCallback.onTransitionToIdle();
+      }
+    }
+    return idle || idleWait;
+  }
+
+  @Override
+  public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {
+    this.resourceCallback = resourceCallback;
+    // on main thread here.
+    queueInterrogator = new QueueInterrogator(monitoredLooper);
+
+    // must load idle handlers from monitored looper thread.
+    IdleHandler idleHandler = new ResourceCallbackIdleHandler(resourceCallback, queueInterrogator,
+        monitoredHandler);
+
+    checkState(monitoredHandler.postAtFrontOfQueue(new Initializer(idleHandler)),
+          "Monitored looper exiting.");
+  }
+
+  private static class ResourceCallbackIdleHandler implements IdleHandler {
+    private final ResourceCallback resourceCallback;
+    private final QueueInterrogator myInterrogator;
+    private final Handler myHandler;
+
+    ResourceCallbackIdleHandler(ResourceCallback resourceCallback,
+        QueueInterrogator myInterrogator, Handler myHandler) {
+      this.resourceCallback = checkNotNull(resourceCallback);
+      this.myInterrogator = checkNotNull(myInterrogator);
+      this.myHandler = checkNotNull(myHandler);
+    }
+
+    @Override
+    public boolean queueIdle() {
+      // invoked on the monitored looper thread.
+      QueueState queueState = myInterrogator.determineQueueState();
+      if (queueState == QueueState.EMPTY || queueState == QueueState.TASK_DUE_LONG) {
+        // no block and no task coming 'shortly'.
+        resourceCallback.onTransitionToIdle();
+      } else if (queueState == QueueState.BARRIER) {
+        // send a sentinal message that'll cause us to queueIdle again once the
+        // block is lifted.
+        myHandler.sendEmptyMessage(-1);
+      }
+
+      return true;
+    }
+  }
+
+  private static class Initializer implements Runnable {
+    private final IdleHandler myIdleHandler;
+
+    Initializer(IdleHandler myIdleHandler) {
+      this.myIdleHandler = checkNotNull(myIdleHandler);
+    }
+
+    @Override
+    public void run() {
+      // on monitored looper thread.
+      Looper.myQueue().addIdleHandler(myIdleHandler);
+    }
+  }
+
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/MainThread.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/MainThread.java
new file mode 100644
index 0000000..c431f48
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/MainThread.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.base;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import javax.inject.Qualifier;
+
+/**
+ * Annotates an Executor that executes tasks on the main thread
+ */
+@Qualifier
+@Retention(RetentionPolicy.RUNTIME)
+public @interface MainThread { }
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/QueueInterrogator.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/QueueInterrogator.java
new file mode 100644
index 0000000..cd63eb8
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/QueueInterrogator.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.base;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.base.Throwables.propagate;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.MessageQueue;
+import android.os.SystemClock;
+import android.util.Log;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+
+/**
+ * Isolates the nasty details of touching the message queue.
+ */
+final class QueueInterrogator {
+
+  enum QueueState { EMPTY, TASK_DUE_SOON, TASK_DUE_LONG, BARRIER };
+  
+  private static final String TAG = "QueueInterrogator";
+
+  private static final Method messageQueueNextMethod;
+  private static final Field messageQueueHeadField;
+  private static final int LOOKAHEAD_MILLIS = 15;
+
+  private final Looper interrogatedLooper;
+  private volatile MessageQueue interrogatedQueue;
+
+  static {
+    Method nextMethod = null;
+    Field headField = null;
+    try {
+      nextMethod = MessageQueue.class.getDeclaredMethod("next");
+      nextMethod.setAccessible(true);
+
+      headField = MessageQueue.class.getDeclaredField("mMessages");
+      headField.setAccessible(true);
+    } catch (IllegalArgumentException e) {
+      nextMethod = null;
+      headField = null;
+      Log.e(TAG, "Could not initialize interrogator!", e);
+    } catch (NoSuchFieldException e) {
+      nextMethod = null;
+      headField = null;
+      Log.e(TAG, "Could not initialize interrogator!", e);
+    } catch (NoSuchMethodException e) {
+      nextMethod = null;
+      headField = null;
+      Log.e(TAG, "Could not initialize interrogator!", e);
+    } catch (SecurityException e) {
+      nextMethod = null;
+      headField = null;
+      Log.e(TAG, "Could not initialize interrogator!", e);
+    } finally {
+      messageQueueNextMethod = nextMethod;
+      messageQueueHeadField = headField;
+    }
+  }
+
+  QueueInterrogator(Looper interrogatedLooper) {
+    this.interrogatedLooper = checkNotNull(interrogatedLooper);
+    checkNotNull(messageQueueHeadField);
+    checkNotNull(messageQueueNextMethod);
+  }
+
+  // Only for use by espresso - keep package private.
+  Message getNextMessage() {
+    checkThread();
+
+    if (null == interrogatedQueue) {
+      initializeQueue();
+    }
+
+    try {
+      return (Message) messageQueueNextMethod.invoke(Looper.myQueue());
+    } catch (IllegalAccessException e) {
+      throw propagate(e);
+    } catch (IllegalArgumentException e) {
+      throw propagate(e);
+    } catch (InvocationTargetException e) {
+      throw propagate(e);
+    } catch (SecurityException e) {
+      throw propagate(e);
+    }
+  }
+
+  QueueState determineQueueState() {
+    // may be called from any thread.
+
+    if (null == interrogatedQueue) {
+      initializeQueue();
+    }
+    synchronized (interrogatedQueue) {
+      try {
+        Message head = (Message) messageQueueHeadField.get(interrogatedQueue);
+        if (null == head) {
+          // no messages pending - AT ALL!
+          return QueueState.EMPTY;
+        }
+        if (null == head.getTarget()) {
+          // null target is a sync barrier token.
+          return QueueState.BARRIER;
+        } else {
+          long headWhen = head.getWhen();
+          long nowFuz = SystemClock.uptimeMillis() + LOOKAHEAD_MILLIS;
+
+          if (nowFuz > headWhen) {
+            return QueueState.TASK_DUE_SOON;
+          } else {
+            return QueueState.TASK_DUE_LONG;
+          }
+        }
+      } catch (IllegalAccessException e) {
+        throw propagate(e);
+      }
+    }
+  }
+
+  private void initializeQueue() {
+    if (interrogatedLooper == Looper.myLooper()) {
+      interrogatedQueue = Looper.myQueue();
+    } else {
+      Handler oneShotHandler = new Handler(interrogatedLooper);
+      FutureTask<MessageQueue> queueCapture = new FutureTask<MessageQueue>(
+          new Callable<MessageQueue>() {
+            @Override
+            public MessageQueue call() {
+              return Looper.myQueue();
+            }
+          });
+      oneShotHandler.postAtFrontOfQueue(queueCapture);
+      try {
+        interrogatedQueue = queueCapture.get();
+      } catch (ExecutionException ee) {
+        throw propagate(ee.getCause());
+      } catch (InterruptedException ie) {
+        throw propagate(ie);
+      }
+    }
+  }
+
+  private void checkThread() {
+    checkState(interrogatedLooper == Looper.myLooper(), "Calling from non-owning thread!");
+  }
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/RootViewPicker.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/RootViewPicker.java
new file mode 100644
index 0000000..6870aa3
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/RootViewPicker.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.base;
+
+import static com.google.android.apps.common.testing.ui.espresso.matcher.RootMatchers.isDialog;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.RootMatchers.isFocusable;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.android.apps.common.testing.testrunner.ActivityLifecycleMonitor;
+import com.google.android.apps.common.testing.testrunner.Stage;
+import com.google.android.apps.common.testing.ui.espresso.NoActivityResumedException;
+import com.google.android.apps.common.testing.ui.espresso.NoMatchingRootException;
+import com.google.android.apps.common.testing.ui.espresso.Root;
+import com.google.android.apps.common.testing.ui.espresso.UiController;
+import com.google.common.base.Joiner;
+import com.google.common.collect.Lists;
+
+import android.app.Activity;
+import android.os.Looper;
+import android.util.Log;
+import android.view.View;
+
+import org.hamcrest.Matcher;
+
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.inject.Inject;
+import javax.inject.Provider;
+import javax.inject.Singleton;
+
+/**
+ * Provides the root View of the top-most Window, with which the user can interact. View is
+ * guaranteed to be in a stable state - i.e. not pending any updates from the application.
+ *
+ * This provider can only be accessed from the main thread.
+ */
+@Singleton
+public final class RootViewPicker implements Provider<View> {
+  private static final String TAG = RootViewPicker.class.getSimpleName();
+
+  private final Provider<List<Root>> rootsOracle;
+  private final UiController uiController;
+  private final ActivityLifecycleMonitor activityLifecycleMonitor;
+  private final AtomicReference<Matcher<Root>> rootMatcherRef;
+
+  private List<Root> roots;
+
+  @Inject
+  RootViewPicker(Provider<List<Root>> rootsOracle, UiController uiController,
+      ActivityLifecycleMonitor activityLifecycleMonitor,
+      AtomicReference<Matcher<Root>> rootMatcherRef) {
+    this.rootsOracle = rootsOracle;
+    this.uiController = uiController;
+    this.activityLifecycleMonitor = activityLifecycleMonitor;
+    this.rootMatcherRef = rootMatcherRef;
+  }
+
+  @Override
+  public View get() {
+    checkState(Looper.getMainLooper().equals(Looper.myLooper()), "must be called on main thread.");
+    Matcher<Root> rootMatcher = rootMatcherRef.get();
+
+    Root root = findRoot(rootMatcher);
+
+    // we only want to propagate a root view that the user can interact with and is not
+    // about to relay itself out. An app should be in this state the majority of the time,
+    // if we happen not to be in this state at the moment, process the queue some more
+    // we should come to it quickly enough.
+    int loops = 0;
+
+    while (!isReady(root)) {
+      if (loops < 3) {
+        uiController.loopMainThreadUntilIdle();
+      } else if (loops < 1001) {
+
+        // loopUntil idle effectively is polling and pegs the CPU... if we don't have an update to
+        // process immediately, we might have something coming very very soon.
+        uiController.loopMainThreadForAtLeast(10);
+      } else {
+        // we've waited for the root view to be fully laid out and have window focus
+        // for over 10 seconds. something is wrong.
+        throw new RuntimeException(String.format("Waited for the root of the view hierarchy to have"
+            + " window focus and not be requesting layout for over 10 seconds. If you specified a"
+            + " non default root matcher, it may be picking a root that never takes focus."
+            + " Otherwise, something is seriously wrong. Selected Root:\n%s\n. All Roots:\n%s"
+            , root, Joiner.on("\n").join(roots)));
+      }
+
+      root = findRoot(rootMatcher);
+      loops++;
+    }
+
+    return root.getDecorView();
+  }
+
+  private boolean isReady(Root root) {
+    // Root is ready (i.e. UI is no longer in flux) if layout of the root view is not being
+    // requested and the root view has window focus (if it is focusable).
+    View rootView = root.getDecorView();
+    if (!rootView.isLayoutRequested()) {
+      return rootView.hasWindowFocus() || !isFocusable().matches(root);
+    }
+    return false;
+  }
+
+  private Root findRoot(Matcher<Root> rootMatcher) {
+    waitForAtLeastOneActivityToBeResumed();
+
+    roots = rootsOracle.get();
+
+    // TODO(user): move these checks into the RootsOracle.
+    if (roots.isEmpty()) {
+      // Reflection broke
+      throw new RuntimeException("No root window were discovered.");
+    }
+
+    if (roots.size() > 1) {
+      // Multiple roots only occur:
+      // when multiple activities are in some state of their lifecycle in the application
+      // - we don't care about this, since we only want to interact with the RESUMED
+      // activity, all other activities windows are not visible to the user so, out of
+      // scope.
+      // when a PopupWindow or PopupMenu is used
+      // - this is a case where we definitely want to consider the top most window, since
+      // it probably has the most useful info in it.
+      // when an android.app.dialog is shown
+      // - again, this is getting all the users attention, so it gets the test attention
+      // too.
+      if (Log.isLoggable(TAG, Log.VERBOSE)) {
+        Log.v(TAG, String.format("Multiple windows detected: %s", roots));
+      }
+    }
+
+    List<Root> selectedRoots = Lists.newArrayList();
+    for (Root root : roots) {
+      if (rootMatcher.matches(root)) {
+        selectedRoots.add(root);
+      }
+    }
+
+    if (selectedRoots.isEmpty()) {
+      throw NoMatchingRootException.create(rootMatcher, roots);
+    }
+
+    return reduceRoots(selectedRoots);
+  }
+
+  @SuppressWarnings("unused")
+  private void waitForAtLeastOneActivityToBeResumed() {
+    Collection<Activity> resumedActivities =
+        activityLifecycleMonitor.getActivitiesInStage(Stage.RESUMED);
+    if (resumedActivities.isEmpty()) {
+      uiController.loopMainThreadUntilIdle();
+      resumedActivities = activityLifecycleMonitor.getActivitiesInStage(Stage.RESUMED);
+    }
+    if (resumedActivities.isEmpty()) {
+      List<Activity> activities = Lists.newArrayList();
+      for (Stage s : EnumSet.range(Stage.PRE_ON_CREATE, Stage.RESTARTED)) {
+        activities.addAll(activityLifecycleMonitor.getActivitiesInStage(s));
+      }
+      if (activities.isEmpty()) {
+        throw new RuntimeException("No activities found. Did you forget to launch the activity "
+            + "by calling getActivity() or startActivitySync or similar?");
+      }
+      // well at least there are some activities in the pipeline - lets see if they resume.
+
+      long[] waitTimes =
+          {10, 50, 100, 500, TimeUnit.SECONDS.toMillis(2), TimeUnit.SECONDS.toMillis(30)};
+
+      for (int waitIdx = 0; waitIdx < waitTimes.length; waitIdx++) {
+        Log.w(TAG, "No activity currently resumed - waiting: " + waitTimes[waitIdx]
+            + "ms for one to appear.");
+        uiController.loopMainThreadForAtLeast(waitTimes[waitIdx]);
+        resumedActivities = activityLifecycleMonitor.getActivitiesInStage(Stage.RESUMED);
+        if (!resumedActivities.isEmpty()) {
+          return; // one of the pending activities has resumed
+        }
+      }
+      throw new NoActivityResumedException("No activities in stage RESUMED. Did you forget to "
+          + "launch the activity. (test.getActivity() or similar)?");
+    }
+  }
+
+  private Root reduceRoots(List<Root> subpanels) {
+    Root topSubpanel = subpanels.get(0);
+    if (subpanels.size() >= 1) {
+      for (Root subpanel : subpanels) {
+        if (isDialog().matches(subpanel)) {
+          return subpanel;
+        }
+        if (subpanel.getWindowLayoutParams().get().type
+            > topSubpanel.getWindowLayoutParams().get().type) {
+          topSubpanel = subpanel;
+        }
+      }
+    }
+    return topSubpanel;
+  }
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/RootsOracle.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/RootsOracle.java
new file mode 100644
index 0000000..a284ede
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/RootsOracle.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.base;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.android.apps.common.testing.ui.espresso.Root;
+import com.google.common.collect.Lists;
+
+import android.os.Build;
+import android.os.Looper;
+import android.util.Log;
+import android.view.View;
+import android.view.WindowManager.LayoutParams;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Provider;
+import javax.inject.Singleton;
+
+/**
+ * Provides access to all root views in an application.
+ *
+ * 95% of the time this is unnecessary and we can operate solely on current Activity's root view
+ * as indicated by getWindow().getDecorView(). However in the case of popup windows, menus, and
+ * dialogs the actual view hierarchy we should be operating on is not accessible thru public apis.
+ *
+ * In the spirit of degrading gracefully when new api levels break compatibility, callers should
+ * handle a list of size 0 by assuming getWindow().getDecorView() on the currently resumed activity
+ * is the sole root - this assumption will be correct often enough.
+ *
+ * Obviously, you need to be on the main thread to use this.
+ */
+@Singleton
+final class RootsOracle implements Provider<List<Root>> {
+
+  private static final String TAG = RootsOracle.class.getSimpleName();
+  private static final String WINDOW_MANAGER_IMPL_CLAZZ =
+      "android.view.WindowManagerImpl";
+  private static final String WINDOW_MANAGER_GLOBAL_CLAZZ =
+      "android.view.WindowManagerGlobal";
+  private static final String VIEWS_FIELD = "mViews";
+  private static final String WINDOW_PARAMS_FIELD = "mParams";
+  private static final String GET_DEFAULT_IMPL = "getDefault";
+  private static final String GET_GLOBAL_INSTANCE = "getInstance";
+
+  private final Looper mainLooper;
+  private boolean initialized;
+  private Object windowManagerObj;
+  private Field viewsField;
+  private Field paramsField;
+
+  @Inject
+  RootsOracle(Looper mainLooper) {
+    this.mainLooper = mainLooper;
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public List<Root> get() {
+    checkState(mainLooper.equals(Looper.myLooper()), "must be called on main thread.");
+
+    if (!initialized) {
+      initialize();
+    }
+
+    if (null == windowManagerObj) {
+      Log.w(TAG, "No reflective access to windowmanager object.");
+      return Lists.newArrayList();
+    }
+
+    if (null == viewsField) {
+      Log.w(TAG, "No reflective access to mViews");
+      return Lists.newArrayList();
+    }
+    if (null == paramsField) {
+      Log.w(TAG, "No reflective access to mPArams");
+      return Lists.newArrayList();
+    }
+
+    List<View> views = null;
+    List<LayoutParams> params = null;
+
+    try {
+      if (Build.VERSION.SDK_INT < 19) {
+        views = Arrays.asList((View[]) viewsField.get(windowManagerObj));
+        params = Arrays.asList((LayoutParams[]) paramsField.get(windowManagerObj));
+      } else {
+        views = (List<View>) viewsField.get(windowManagerObj);
+        params = (List<LayoutParams>) paramsField.get(windowManagerObj);
+      }
+    } catch (RuntimeException re) {
+      Log.w(TAG, String.format("Reflective access to %s or %s on %s failed.",
+          viewsField, paramsField, windowManagerObj), re);
+      return Lists.newArrayList();
+    } catch (IllegalAccessException iae) {
+      Log.w(TAG, String.format("Reflective access to %s or %s on %s failed.",
+          viewsField, paramsField, windowManagerObj), iae);
+      return Lists.newArrayList();
+    }
+
+
+    List<Root> roots = Lists.newArrayList();
+    for (int i = views.size() - 1; i > -1; i--) {
+      roots.add(
+          new Root.Builder()
+              .withDecorView(views.get(i))
+              .withWindowLayoutParams(params.get(i))
+              .build());
+    }
+
+    return roots;
+  }
+
+  private void initialize() {
+    initialized = true;
+    String accessClass = Build.VERSION.SDK_INT > 16 ? WINDOW_MANAGER_GLOBAL_CLAZZ
+        : WINDOW_MANAGER_IMPL_CLAZZ;
+    String instanceMethod = Build.VERSION.SDK_INT > 16 ? GET_GLOBAL_INSTANCE : GET_DEFAULT_IMPL;
+
+    try {
+      Class<?> clazz = Class.forName(accessClass);
+      Method getMethod = clazz.getMethod(instanceMethod);
+      windowManagerObj = getMethod.invoke(null);
+      viewsField = clazz.getDeclaredField(VIEWS_FIELD);
+      viewsField.setAccessible(true);
+      paramsField = clazz.getDeclaredField(WINDOW_PARAMS_FIELD);
+      paramsField.setAccessible(true);
+    } catch (InvocationTargetException ite) {
+      Log.e(TAG, String.format("could not invoke: %s on %s", instanceMethod, accessClass),
+        ite.getCause());
+    } catch (ClassNotFoundException cnfe) {
+      Log.e(TAG, String.format("could not find class: %s", accessClass), cnfe);
+    } catch (NoSuchFieldException nsfe) {
+      Log.e(TAG, String.format("could not find field: %s or %s on %s", WINDOW_PARAMS_FIELD,
+          VIEWS_FIELD, accessClass), nsfe);
+    } catch (NoSuchMethodException nsme) {
+      Log.e(TAG, String.format("could not find method: %s on %s", instanceMethod, accessClass),
+        nsme);
+    } catch (RuntimeException re) {
+      Log.e(TAG, String.format("reflective setup failed using obj: %s method: %s field: %s",
+        accessClass, instanceMethod, VIEWS_FIELD), re);
+    } catch (IllegalAccessException iae) {
+      Log.e(TAG, String.format("reflective setup failed using obj: %s method: %s field: %s",
+        accessClass, instanceMethod, VIEWS_FIELD), iae);
+    }
+  }
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/SdkAsyncTask.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/SdkAsyncTask.java
new file mode 100644
index 0000000..b28255e
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/SdkAsyncTask.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.base;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import javax.inject.Qualifier;
+
+/**
+ * Annotates a AsyncTaskMonitor as monitoring the SdkAsyncTask pool
+ */
+@Qualifier
+@Retention(RetentionPolicy.RUNTIME)
+@interface SdkAsyncTask { }
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/ThreadPoolExecutorExtractor.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/ThreadPoolExecutorExtractor.java
new file mode 100644
index 0000000..1a719a3
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/ThreadPoolExecutorExtractor.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.base;
+
+import com.google.common.base.Optional;
+
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+
+import java.lang.reflect.Field;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.ThreadPoolExecutor;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+/**
+ * Extracts ThreadPoolExecutors used by pieces of android.
+ *
+ * We do some work to ensure that we load the classes containing these thread pools
+ * on the main thread, since they may have static initialization that assumes access
+ * to the main looper.
+ */
+@Singleton
+final class ThreadPoolExecutorExtractor {
+  private static final String ASYNC_TASK_CLASS_NAME = "android.os.AsyncTask";
+  private static final String MODERN_ASYNC_TASK_CLASS_NAME =
+      "android.support.v4.content.ModernAsyncTask";
+  private static final String MODERN_ASYNC_TASK_FIELD_NAME = "THREAD_POOL_EXECUTOR";
+  private static final String LEGACY_ASYNC_TASK_FIELD_NAME = "sExecutor";
+  private final Handler mainHandler;
+
+  @Inject
+  ThreadPoolExecutorExtractor(Looper looper) {
+    mainHandler = new Handler(looper);
+  }
+
+
+  public ThreadPoolExecutor getAsyncTaskThreadPool() {
+    FutureTask<Optional<ThreadPoolExecutor>> getTask = null;
+    if (Build.VERSION.SDK_INT < 11) {
+      getTask = new FutureTask<Optional<ThreadPoolExecutor>>(LEGACY_ASYNC_TASK_EXECUTOR);
+    } else {
+      getTask = new FutureTask<Optional<ThreadPoolExecutor>>(POST_HONEYCOMB_ASYNC_TASK_EXECUTOR);
+    }
+
+    try {
+      return runOnMainThread(getTask).get().get();
+    } catch (InterruptedException ie) {
+      throw new RuntimeException("Interrupted while trying to get the async task executor!", ie);
+    } catch (ExecutionException ee) {
+      throw new RuntimeException(ee.getCause());
+    }
+  }
+
+  public Optional<ThreadPoolExecutor> getCompatAsyncTaskThreadPool() {
+    try {
+      return runOnMainThread(
+          new FutureTask<Optional<ThreadPoolExecutor>>(MODERN_ASYNC_TASK_EXTRACTOR)).get();
+    } catch (InterruptedException ie) {
+      throw new RuntimeException("Interrupted while trying to get the compat async executor!", ie);
+    } catch (ExecutionException ee) {
+      throw new RuntimeException(ee.getCause());
+    }
+  }
+
+  private <T> FutureTask<T> runOnMainThread(final FutureTask<T> futureToRun) {
+    if (Looper.myLooper() != Looper.getMainLooper()) {
+      final CountDownLatch latch = new CountDownLatch(1);
+      mainHandler.post(new Runnable() {
+        @Override
+        public void run() {
+          try {
+            futureToRun.run();
+          } finally {
+            latch.countDown();
+          }
+        }
+      });
+      try {
+        latch.await();
+      } catch (InterruptedException ie) {
+        if (!futureToRun.isDone()) {
+          throw new RuntimeException("Interrupted while waiting for task to complete.");
+        }
+      }
+    } else {
+      futureToRun.run();
+    }
+
+    return futureToRun;
+  }
+
+  private static final Callable<Optional<ThreadPoolExecutor>> MODERN_ASYNC_TASK_EXTRACTOR =
+      new Callable<Optional<ThreadPoolExecutor>>() {
+        @Override
+        public Optional<ThreadPoolExecutor> call() throws Exception {
+          try {
+            Class<?> modernClazz = Class.forName(MODERN_ASYNC_TASK_CLASS_NAME);
+            Field executorField = modernClazz.getField(MODERN_ASYNC_TASK_FIELD_NAME);
+            return Optional.of((ThreadPoolExecutor) executorField.get(null));
+          } catch (ClassNotFoundException cnfe) {
+            return Optional.<ThreadPoolExecutor>absent();
+          }
+        }
+      };
+
+  private static final Callable<Class<?>> LOAD_ASYNC_TASK_CLASS =
+      new Callable<Class<?>>() {
+        @Override
+        public Class<?> call() throws Exception {
+          return Class.forName(ASYNC_TASK_CLASS_NAME);
+        }
+      };
+
+  private static final Callable<Optional<ThreadPoolExecutor>> LEGACY_ASYNC_TASK_EXECUTOR =
+      new Callable<Optional<ThreadPoolExecutor>>() {
+        @Override
+        public Optional<ThreadPoolExecutor> call() throws Exception {
+          Field executorField = LOAD_ASYNC_TASK_CLASS.call()
+              .getDeclaredField(LEGACY_ASYNC_TASK_FIELD_NAME);
+          executorField.setAccessible(true);
+          return Optional.of((ThreadPoolExecutor) executorField.get(null));
+        }
+      };
+
+  private static final Callable<Optional<ThreadPoolExecutor>> POST_HONEYCOMB_ASYNC_TASK_EXECUTOR =
+      new Callable<Optional<ThreadPoolExecutor>>() {
+        @Override
+        public Optional<ThreadPoolExecutor> call() throws Exception {
+          Field executorField = LOAD_ASYNC_TASK_CLASS.call()
+              .getField(MODERN_ASYNC_TASK_FIELD_NAME);
+          return Optional.of((ThreadPoolExecutor) executorField.get(null));
+        }
+      };
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/UiControllerImpl.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/UiControllerImpl.java
new file mode 100644
index 0000000..c1aaa5c
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/UiControllerImpl.java
@@ -0,0 +1,535 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.base;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.base.Throwables.propagate;
+
+import com.google.android.apps.common.testing.ui.espresso.IdlingPolicies;
+import com.google.android.apps.common.testing.ui.espresso.IdlingPolicy;
+import com.google.android.apps.common.testing.ui.espresso.InjectEventSecurityException;
+import com.google.android.apps.common.testing.ui.espresso.UiController;
+import com.google.android.apps.common.testing.ui.espresso.base.IdlingResourceRegistry.IdleNotificationCallback;
+import com.google.android.apps.common.testing.ui.espresso.base.QueueInterrogator.QueueState;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Optional;
+import com.google.common.collect.Lists;
+
+import android.annotation.SuppressLint;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+import java.util.BitSet;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.FutureTask;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+/**
+ * Implementation of {@link UiController}.
+ */
+@Singleton
+final class UiControllerImpl implements UiController, Handler.Callback {
+
+  private static final String TAG = UiControllerImpl.class.getSimpleName();
+
+  private static final Callable<Void> NO_OP = new Callable<Void>() {
+    @Override
+    public Void call() {
+      return null;
+    }
+  };
+
+  /**
+   * Responsible for signaling a particular condition is met / verifying that signal.
+   */
+  enum IdleCondition {
+      DELAY_HAS_PAST,
+      ASYNC_TASKS_HAVE_IDLED,
+      COMPAT_TASKS_HAVE_IDLED,
+      KEY_INJECT_HAS_COMPLETED,
+      MOTION_INJECTION_HAS_COMPLETED,
+      DYNAMIC_TASKS_HAVE_IDLED;
+
+      /**
+       * Checks whether this condition has been signaled.
+       */
+      public boolean isSignaled(BitSet conditionSet) {
+        return conditionSet.get(ordinal());
+      }
+
+      /**
+       * Resets the signal state for this condition.
+       */
+      public void reset(BitSet conditionSet) {
+        conditionSet.set(ordinal(), false);
+      }
+
+      /**
+       * Creates a message that when sent will raise the signal of this condition.
+       */
+      public Message createSignal(Handler handler, int myGeneration) {
+        return Message.obtain(handler, ordinal(), myGeneration, 0, null);
+      }
+
+      /**
+       * Handles a message that is raising a signal and updates the condition set accordingly.
+       * Messages from a previous generation will be ignored.
+       */
+      public static boolean handleMessage(Message message, BitSet conditionSet,
+          int currentGeneration) {
+        IdleCondition [] allConditions = values();
+        if (message.what < 0 || message.what >= allConditions.length) {
+          return false;
+        } else {
+          IdleCondition condition = allConditions[message.what];
+          if (message.arg1 == currentGeneration) {
+            condition.signal(conditionSet);
+          } else {
+            Log.w(TAG, "ignoring signal of: " + condition + " from previous generation: " +
+                message.arg1 + " current generation: " + currentGeneration);
+          }
+          return true;
+        }
+      }
+
+      public static BitSet createConditionSet() {
+        return new BitSet(values().length);
+      }
+
+      /**
+       * Requests that the given bitset be updated to indicate that this condition has been
+       * signaled.
+       */
+      protected void signal(BitSet conditionSet) {
+        conditionSet.set(ordinal());
+      }
+  }
+
+  private final EventInjector eventInjector;
+  private final BitSet conditionSet;
+  private final AsyncTaskPoolMonitor asyncTaskMonitor;
+  private final Optional<AsyncTaskPoolMonitor> compatTaskMonitor;
+  private final IdlingResourceRegistry idlingResourceRegistry;
+  private final ExecutorService keyEventExecutor = Executors.newSingleThreadExecutor();
+  private final QueueInterrogator queueInterrogator;
+  private final Looper mainLooper;
+
+  private Handler controllerHandler;
+  // only updated on main thread.
+  private boolean looping = false;
+  private int generation = 0;
+
+  @VisibleForTesting
+  @Inject
+  UiControllerImpl(EventInjector eventInjector,
+      @SdkAsyncTask AsyncTaskPoolMonitor asyncTaskMonitor,
+      @CompatAsyncTask Optional<AsyncTaskPoolMonitor> compatTaskMonitor,
+      IdlingResourceRegistry registry,
+      Looper mainLooper) {
+    this.eventInjector = checkNotNull(eventInjector);
+    this.asyncTaskMonitor = checkNotNull(asyncTaskMonitor);
+    this.compatTaskMonitor = checkNotNull(compatTaskMonitor);
+    this.conditionSet = IdleCondition.createConditionSet();
+    this.idlingResourceRegistry = checkNotNull(registry);
+    this.mainLooper = checkNotNull(mainLooper);
+    this.queueInterrogator = new QueueInterrogator(mainLooper);
+  }
+
+  @SuppressWarnings("deprecation")
+  @Override
+  public boolean injectKeyEvent(final KeyEvent event) throws InjectEventSecurityException {
+    checkNotNull(event);
+    checkState(Looper.myLooper() == mainLooper, "Expecting to be on main thread!");
+    initialize();
+    loopMainThreadUntilIdle();
+
+    FutureTask<Boolean> injectTask = new SignalingTask<Boolean>(
+        new Callable<Boolean>() {
+          @Override
+          public Boolean call() throws Exception {
+            return eventInjector.injectKeyEvent(event);
+          }
+        },
+        IdleCondition.KEY_INJECT_HAS_COMPLETED,
+        generation);
+
+    // Inject the key event.
+    keyEventExecutor.submit(injectTask);
+
+    loopUntil(IdleCondition.KEY_INJECT_HAS_COMPLETED);
+
+    try {
+      checkState(injectTask.isDone(), "Key injection was signaled - but it wasnt done.");
+      return injectTask.get();
+    } catch (ExecutionException ee) {
+      if (ee.getCause() instanceof InjectEventSecurityException) {
+        throw (InjectEventSecurityException) ee.getCause();
+      } else {
+        throw new RuntimeException(ee.getCause());
+      }
+    } catch (InterruptedException neverHappens) {
+      // we only call get() after done() is signaled.
+      // we should never block.
+      throw new RuntimeException("impossible.", neverHappens);
+    }
+  }
+
+  @Override
+  public boolean injectMotionEvent(final MotionEvent event) throws InjectEventSecurityException {
+    checkNotNull(event);
+    checkState(Looper.myLooper() == mainLooper, "Expecting to be on main thread!");
+    initialize();
+
+    FutureTask<Boolean> injectTask = new SignalingTask<Boolean>(
+        new Callable<Boolean>() {
+          @Override
+          public Boolean call() throws Exception {
+            return eventInjector.injectMotionEvent(event);
+          }
+        },
+        IdleCondition.MOTION_INJECTION_HAS_COMPLETED,
+        generation);
+    keyEventExecutor.submit(injectTask);
+    loopUntil(IdleCondition.MOTION_INJECTION_HAS_COMPLETED);
+    try {
+      checkState(injectTask.isDone(), "Key injection was signaled - but it wasnt done.");
+      return injectTask.get();
+    } catch (ExecutionException ee) {
+      if (ee.getCause() instanceof InjectEventSecurityException) {
+        throw (InjectEventSecurityException) ee.getCause();
+      } else {
+        throw propagate(ee.getCause() != null ? ee.getCause() : ee);
+      }
+    } catch (InterruptedException neverHappens) {
+      // we only call get() after done() is signaled.
+      // we should never block.
+      throw propagate(neverHappens);
+    } finally {
+      loopMainThreadUntilIdle();
+    }
+  }
+
+  @Override
+  public boolean injectString(String str) throws InjectEventSecurityException {
+    checkNotNull(str);
+    checkState(Looper.myLooper() == mainLooper, "Expecting to be on main thread!");
+    initialize();
+
+    // No-op if string is empty.
+    if (str.length() == 0) {
+      Log.w(TAG, "Supplied string is empty resulting in no-op (nothing is typed).");
+      return true;
+    }
+
+    boolean eventInjected = false;
+    KeyCharacterMap keyCharacterMap = getKeyCharacterMap();
+
+    // TODO(user): Investigate why not use (as suggested in javadoc of keyCharacterMap.getEvents):
+    // http://developer.android.com/reference/android/view/KeyEvent.html#KeyEvent(long,
+    // java.lang.String, int, int)
+    KeyEvent[] events = keyCharacterMap.getEvents(str.toCharArray());
+    checkNotNull(events, "Failed to get events for string " + str);
+    Log.d(TAG, String.format("Injecting string: \"%s\"", str));
+
+    for (KeyEvent event : events) {
+      checkNotNull(event, String.format("Failed to get event for character (%c) with key code (%s)",
+          event.getKeyCode(), event.getUnicodeChar()));
+
+      eventInjected = false;
+      for (int attempts = 0; !eventInjected && attempts < 4; attempts++) {
+        attempts++;
+
+        // We have to change the time of an event before injecting it because
+        // all KeyEvents returned by KeyCharacterMap.getEvents() have the same
+        // time stamp and the system rejects too old events. Hence, it is
+        // possible for an event to become stale before it is injected if it
+        // takes too long to inject the preceding ones.
+        event = KeyEvent.changeTimeRepeat(event, SystemClock.uptimeMillis(), 0);
+        eventInjected = injectKeyEvent(event);
+      }
+
+      if (!eventInjected) {
+        Log.e(TAG, String.format("Failed to inject event for character (%c) with key code (%s)",
+            event.getUnicodeChar(), event.getKeyCode()));
+        break;
+      }
+    }
+
+    return eventInjected;
+  }
+
+  @SuppressLint("InlinedApi")
+  @VisibleForTesting
+  @SuppressWarnings("deprecation")
+  public static KeyCharacterMap getKeyCharacterMap() {
+    KeyCharacterMap keyCharacterMap = null;
+
+    // KeyCharacterMap.VIRTUAL_KEYBOARD is present from API11.
+    // For earlier APIs we use KeyCharacterMap.BUILT_IN_KEYBOARD
+    if (Build.VERSION.SDK_INT < 11) {
+      keyCharacterMap = KeyCharacterMap.load(KeyCharacterMap.BUILT_IN_KEYBOARD);
+    } else {
+      keyCharacterMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
+    }
+    return keyCharacterMap;
+  }
+
+
+  @Override
+  public void loopMainThreadUntilIdle() {
+    initialize();
+    checkState(Looper.myLooper() == mainLooper, "Expecting to be on main thread!");
+    do {
+      EnumSet<IdleCondition> condChecks = EnumSet.noneOf(IdleCondition.class);
+      if (!asyncTaskMonitor.isIdleNow()) {
+        asyncTaskMonitor.notifyWhenIdle(new SignalingTask<Void>(NO_OP,
+            IdleCondition.ASYNC_TASKS_HAVE_IDLED, generation));
+
+        condChecks.add(IdleCondition.ASYNC_TASKS_HAVE_IDLED);
+      }
+
+      if (!compatIdle()) {
+        compatTaskMonitor.get().notifyWhenIdle(new SignalingTask<Void>(NO_OP,
+            IdleCondition.COMPAT_TASKS_HAVE_IDLED, generation));
+        condChecks.add(IdleCondition.COMPAT_TASKS_HAVE_IDLED);
+      }
+
+      if (!idlingResourceRegistry.allResourcesAreIdle()) {
+        final IdlingPolicy warning = IdlingPolicies.getDynamicIdlingResourceWarningPolicy();
+        final IdlingPolicy error = IdlingPolicies.getDynamicIdlingResourceErrorPolicy();
+        final SignalingTask<Void> idleSignal = new SignalingTask<Void>(NO_OP,
+            IdleCondition.DYNAMIC_TASKS_HAVE_IDLED, generation);
+        idlingResourceRegistry.notifyWhenAllResourcesAreIdle(new IdleNotificationCallback() {
+          @Override
+          public void resourcesStillBusyWarning(List<String> busyResourceNames) {
+            warning.handleTimeout(busyResourceNames, "IdlingResources are still busy!");
+          }
+
+          @Override
+          public void resourcesHaveTimedOut(List<String> busyResourceNames) {
+            error.handleTimeout(busyResourceNames, "IdlingResources have timed out!");
+            controllerHandler.post(idleSignal);
+          }
+
+          @Override
+          public void allResourcesIdle() {
+            controllerHandler.post(idleSignal);
+          }
+        });
+        condChecks.add(IdleCondition.DYNAMIC_TASKS_HAVE_IDLED);
+      }
+
+      try {
+        loopUntil(condChecks);
+      } finally {
+        asyncTaskMonitor.cancelIdleMonitor();
+        if (compatTaskMonitor.isPresent()) {
+          compatTaskMonitor.get().cancelIdleMonitor();
+        }
+        idlingResourceRegistry.cancelIdleMonitor();
+      }
+    } while (!asyncTaskMonitor.isIdleNow() || !compatIdle()
+        || !idlingResourceRegistry.allResourcesAreIdle());
+
+  }
+
+  private boolean compatIdle() {
+    if (compatTaskMonitor.isPresent()) {
+      return compatTaskMonitor.get().isIdleNow();
+    } else {
+      return true;
+    }
+  }
+
+  @Override
+  public void loopMainThreadForAtLeast(long millisDelay) {
+    initialize();
+    checkState(Looper.myLooper() == mainLooper, "Expecting to be on main thread!");
+    checkState(!IdleCondition.DELAY_HAS_PAST.isSignaled(conditionSet), "recursion detected!");
+
+    checkArgument(millisDelay > 0);
+    controllerHandler.postDelayed(new SignalingTask(NO_OP, IdleCondition.DELAY_HAS_PAST,
+          generation),
+        millisDelay);
+    loopUntil(IdleCondition.DELAY_HAS_PAST);
+    loopMainThreadUntilIdle();
+  }
+
+  @Override
+  public boolean handleMessage(Message msg) {
+    if (!IdleCondition.handleMessage(msg, conditionSet, generation)) {
+      Log.i(TAG, "Unknown message type: " + msg);
+      return false;
+    } else {
+      return true;
+    }
+  }
+
+  private void loopUntil(IdleCondition condition) {
+    loopUntil(EnumSet.of(condition));
+  }
+
+  /**
+   * Loops the main thread until all IdleConditions have been signaled.
+   *
+   * Once they've been signaled, the conditions are reset and the generation value
+   * is incremented.
+   *
+   * Signals should only be raised thru SignalingTask instances, and care should be
+   * taken to ensure that the signaling task is created before loopUntil is called.
+   *
+   * Good:
+   * idlingType.runOnIdle(new SignalingTask(NO_OP, IdleCondition.MY_IDLE_CONDITION, generation));
+   * loopUntil(IdleCondition.MY_IDLE_CONDITION);
+   *
+   * Bad:
+   * idlingType.runOnIdle(new CustomCallback() {
+   *   @Override
+   *   public void itsDone() {
+   *     // oh no - The creation of this signaling task is delayed until this method is
+   *     // called, so it will not have the right value for generation.
+   *     new SignalingTask(NO_OP, IdleCondition.MY_IDLE_CONDITION, generation).run();
+   *  }
+   * })
+   * loopUntil(IdleCondition.MY_IDLE_CONDITION);
+   */
+  private void loopUntil(EnumSet<IdleCondition> conditions) {
+    checkState(!looping, "Recursive looping detected!");
+    looping = true;
+    IdlingPolicy masterIdlePolicy = IdlingPolicies.getMasterIdlingPolicy();
+    try {
+      int loopCount = 0;
+      long start = SystemClock.uptimeMillis();
+      long end = start + masterIdlePolicy.getIdleTimeoutUnit().toMillis(
+          masterIdlePolicy.getIdleTimeout());
+      while (SystemClock.uptimeMillis() < end) {
+        boolean conditionsMet = true;
+        boolean shouldLogConditionState = loopCount > 0 && loopCount % 100 == 0;
+
+        for (IdleCondition condition : conditions) {
+          if (!condition.isSignaled(conditionSet)) {
+            conditionsMet = false;
+            if (shouldLogConditionState) {
+              Log.w(TAG, "Waiting for: " + condition.name() + " for " + loopCount + " iterations.");
+            } else {
+              break;
+            }
+          }
+        }
+
+        if (conditionsMet) {
+          QueueState queueState = queueInterrogator.determineQueueState();
+          if (queueState == QueueState.EMPTY || queueState == QueueState.TASK_DUE_LONG) {
+            return;
+          } else {
+            Log.v(
+                "ESP_TRACE",
+
+                "Barrier detected or task avaliable for running shortly.");
+          }
+        }
+
+        Message message = queueInterrogator.getNextMessage();
+        String callbackString = "unknown";
+        String messageString = "unknown";
+        try {
+          if (null == message.getCallback()) {
+            callbackString = "no callback.";
+          } else {
+            callbackString = message.getCallback().toString();
+          }
+          messageString = message.toString();
+        } catch (NullPointerException e) {
+          /*
+           * Ignore. android.app.ActivityThread$ActivityClientRecord#toString() fails for API level
+           * 15.
+           */
+        }
+
+        Log.v(
+            "ESP_TRACE",
+            String.format("%s: MessageQueue.next(): %s, with target: %s, callback: %s", TAG,
+              messageString, message.getTarget().getClass().getCanonicalName(), callbackString));
+        message.getTarget().dispatchMessage(message);
+        message.recycle();
+        loopCount++;
+      }
+      List<String> idleConditions = Lists.newArrayList();
+      for (IdleCondition condition : conditions) {
+        if (!condition.isSignaled(conditionSet)) {
+          idleConditions.add(condition.name());
+        }
+      }
+      masterIdlePolicy.handleTimeout(idleConditions, String.format(
+          "Looped for %s iterations over %s %s.", loopCount, masterIdlePolicy.getIdleTimeout(),
+          masterIdlePolicy.getIdleTimeoutUnit().name()));
+    } finally {
+      looping = false;
+      generation++;
+      for (IdleCondition condition : conditions) {
+        condition.reset(conditionSet);
+      }
+    }
+  }
+
+
+  private void initialize() {
+    if (controllerHandler == null) {
+      controllerHandler = new Handler(this);
+    }
+  }
+
+
+  /**
+   * Encapsulates posting a signal message to update the conditions set after a task has
+   * executed.
+   */
+  private class SignalingTask<T> extends FutureTask<T> {
+
+    private final IdleCondition condition;
+    private final int myGeneration;
+
+    public SignalingTask(Callable<T> callable, IdleCondition condition, int myGeneration) {
+      super(callable);
+      this.condition = checkNotNull(condition);
+      this.myGeneration = myGeneration;
+    }
+
+    @Override
+    protected void done() {
+      controllerHandler.sendMessage(condition.createSignal(controllerHandler, myGeneration));
+    }
+
+  }
+
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/ViewFinderImpl.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/ViewFinderImpl.java
new file mode 100644
index 0000000..30e0658
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/ViewFinderImpl.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.base;
+
+import static com.google.android.apps.common.testing.ui.espresso.util.TreeIterables.breadthFirstViewTraversal;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.android.apps.common.testing.ui.espresso.AmbiguousViewMatcherException;
+import com.google.android.apps.common.testing.ui.espresso.NoMatchingViewException;
+import com.google.android.apps.common.testing.ui.espresso.ViewFinder;
+import com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers;
+import com.google.common.base.Joiner;
+import com.google.common.base.Optional;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Iterators;
+import com.google.common.collect.Lists;
+
+import android.os.Looper;
+import android.view.View;
+import android.widget.AdapterView;
+
+import org.hamcrest.Matcher;
+
+import java.util.Iterator;
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Provider;
+
+/**
+ * Implementation of {@link ViewFinder}.
+ */
+// TODO(user): in the future we may want to collect stats here about the size of the view
+// hierarchy, average matcher execution time, warn when matchers take too long to execute, etc.
+public final class ViewFinderImpl implements ViewFinder {
+
+  private final Matcher<View> viewMatcher;
+  private final Provider<View> rootViewProvider;
+
+  @Inject
+  ViewFinderImpl(Matcher<View> viewMatcher, Provider<View> rootViewProvider) {
+    this.viewMatcher = viewMatcher;
+    this.rootViewProvider = rootViewProvider;
+  }
+
+  @Override
+  public View getView() throws AmbiguousViewMatcherException, NoMatchingViewException {
+    checkMainThread();
+    final Predicate<View> matcherPredicate = new MatcherPredicateAdapter<View>(
+        checkNotNull(viewMatcher));
+
+    View root = rootViewProvider.get();
+    Iterator<View> matchedViewIterator = Iterables.filter(
+        breadthFirstViewTraversal(root),
+        matcherPredicate).iterator();
+
+    View matchedView = null;
+
+    while (matchedViewIterator.hasNext()) {
+      if (matchedView != null) {
+        // Ambiguous!
+        throw new AmbiguousViewMatcherException.Builder()
+            .withViewMatcher(viewMatcher)
+            .withRootView(root)
+            .withView1(matchedView)
+            .withView2(matchedViewIterator.next())
+            .withOtherAmbiguousViews(Iterators.toArray(matchedViewIterator, View.class))
+            .build();
+      } else {
+        matchedView = matchedViewIterator.next();
+      }
+    }
+    if (null == matchedView) {
+      final Predicate<View> adapterViewPredicate = new MatcherPredicateAdapter<View>(
+          ViewMatchers.isAssignableFrom(AdapterView.class));
+      List<View> adapterViews = Lists.newArrayList(
+          Iterables.filter(breadthFirstViewTraversal(root), adapterViewPredicate).iterator());
+      if (adapterViews.isEmpty()) {
+        throw new NoMatchingViewException.Builder()
+            .withViewMatcher(viewMatcher)
+            .withRootView(root)
+            .build();
+      }
+
+      String warning = String.format("\nIf the target view is not part of the view hierarchy, you "
+        + "may need to use Espresso.onData to load it from one of the following AdapterViews:%s"
+        , Joiner.on("\n- ").join(adapterViews));
+      throw new NoMatchingViewException.Builder()
+          .withViewMatcher(viewMatcher)
+          .withRootView(root)
+          .withAdapterViews(adapterViews)
+          .withAdapterViewWarning(Optional.of(warning))
+          .build();
+    } else {
+      return matchedView;
+    }
+  }
+
+  private void checkMainThread() {
+    checkState(Thread.currentThread().equals(Looper.getMainLooper().getThread()),
+        "Executing a query on the view hierarchy outside of the main thread (on: %s)",
+        Thread.currentThread().getName());
+  }
+
+  private static class MatcherPredicateAdapter<T> implements Predicate<T> {
+    private final Matcher<? super T> matcher;
+
+    private MatcherPredicateAdapter(Matcher<? super T> matcher) {
+      this.matcher = checkNotNull(matcher);
+    }
+
+    @Override
+    public boolean apply(T input) {
+      return matcher.matches(input);
+    }
+  }
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/WindowManagerEventInjectionStrategy.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/WindowManagerEventInjectionStrategy.java
new file mode 100644
index 0000000..05792e7
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/WindowManagerEventInjectionStrategy.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.base;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.base.Throwables.propagate;
+
+import com.google.android.apps.common.testing.ui.espresso.InjectEventSecurityException;
+
+import android.os.Build;
+import android.os.IBinder;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * An {@link EventInjectionStrategy} that uses the window manager to inject {@link MotionEvent}s.
+ * This strategy supports API level 15 and below.
+ */
+final class WindowManagerEventInjectionStrategy implements EventInjectionStrategy {
+  private static final String TAG = WindowManagerEventInjectionStrategy.class.getSimpleName();
+
+
+  WindowManagerEventInjectionStrategy() {
+    checkState(Build.VERSION.SDK_INT >= 7 && Build.VERSION.SDK_INT <= 15, "Unsupported API level.");
+  }
+
+  // Reflection members.
+  private boolean initComplete;
+  private Object wmInstance;
+  private Method injectInputKeyEventMethod;
+  private Method injectInputMotionEventMethod;
+
+  void initialize() {
+    if (initComplete) {
+      return;
+    }
+
+    try {
+      Log.d(TAG, "Trying to create injection strategy.");
+
+      Class<?> serviceManagerClassObj = Class.forName("android.os.ServiceManager");
+      Method windowServiceMethod =
+          serviceManagerClassObj.getDeclaredMethod("getService", String.class);
+      windowServiceMethod.setAccessible(true);
+
+      Object windowServiceBinderObj = windowServiceMethod.invoke(serviceManagerClassObj, "window");
+
+      Class<?> windowManagerStubObject = Class.forName("android.view.IWindowManager$Stub");
+      Method asInterfaceMethod =
+          windowManagerStubObject.getDeclaredMethod("asInterface", IBinder.class);
+      asInterfaceMethod.setAccessible(true);
+
+      wmInstance = asInterfaceMethod.invoke(windowManagerStubObject, windowServiceBinderObj);
+
+      injectInputMotionEventMethod = wmInstance.getClass()
+          .getDeclaredMethod("injectPointerEvent", MotionEvent.class, Boolean.TYPE);
+      injectInputMotionEventMethod.setAccessible(true);
+
+      injectInputKeyEventMethod =
+          wmInstance.getClass().getDeclaredMethod("injectKeyEvent", KeyEvent.class, Boolean.TYPE);
+      injectInputMotionEventMethod.setAccessible(true);
+
+      initComplete = true;
+    } catch (ClassNotFoundException e) {
+      propagate(e);
+    } catch (IllegalAccessException e) {
+      propagate(e);
+    } catch (IllegalArgumentException e) {
+      propagate(e);
+    } catch (InvocationTargetException e) {
+      propagate(e);
+    } catch (NoSuchMethodException e) {
+      propagate(e);
+    } catch (SecurityException e) {
+      propagate(e);
+    }
+  }
+
+  @Override
+  public boolean injectKeyEvent(KeyEvent keyEvent) throws InjectEventSecurityException {
+    try {
+      // From javadoc of com.android.server.WindowManagerService.injectKeyEvent:
+      // @param sync If true, wait for the event to be completed before returning to the caller.
+      // @return true if event was dispatched, false if it was dropped for any reason
+      //
+      // Key events are delivered OFF the main thread, and we block until they are processed.
+      return (Boolean) injectInputKeyEventMethod.invoke(wmInstance, keyEvent, true);
+    } catch (IllegalAccessException e) {
+      propagate(e);
+    } catch (IllegalArgumentException e) {
+      propagate(e);
+    } catch (InvocationTargetException e) {
+      Throwable cause = e.getCause();
+      if (cause instanceof SecurityException) {
+        throw new InjectEventSecurityException(cause);
+      }
+      propagate(e);
+    } catch (SecurityException e) {
+      throw new InjectEventSecurityException(e);
+    }
+    return false;
+  }
+
+  @Override
+  public boolean injectMotionEvent(MotionEvent motionEvent) throws InjectEventSecurityException {
+    try {
+      // From javadoc of com.android.server.WindowManagerService.injectKeyEvent:
+      // @param sync If true, wait for the event to be completed before returning to the caller.
+      // @return true if event was dispatched, false if it was dropped for any reason
+      //
+      // We inject the pointer with sync=true to ensure the event is dispatched before control
+      // is returned to our code.
+      return (Boolean) injectInputMotionEventMethod.invoke(
+          wmInstance,
+          motionEvent,
+          true /* sync */);
+    } catch (IllegalAccessException e) {
+      propagate(e);
+    } catch (IllegalArgumentException e) {
+      propagate(e);
+    } catch (InvocationTargetException e) {
+      Throwable cause = e.getCause();
+      if (cause instanceof SecurityException) {
+        throw new InjectEventSecurityException(cause);
+      }
+      propagate(e);
+    } catch (SecurityException e) {
+      throw new InjectEventSecurityException(e);
+    }
+    return false;
+  }
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/contrib/CountingIdlingResource.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/contrib/CountingIdlingResource.java
new file mode 100644
index 0000000..c67f199
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/contrib/CountingIdlingResource.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.contrib;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.android.apps.common.testing.ui.espresso.IdlingResource;
+
+import android.os.SystemClock;
+import android.util.Log;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * An implementation of {@link IdlingResource} that determines idleness by maintaining an internal
+ * counter. When the counter is 0 - it is considered to be idle, when it is non-zero it is not
+ * idle. This is very similar to the way a {@link java.util.concurrent.Semaphore} behaves.
+ * <p>
+ * The counter may be incremented or decremented from any thread. If it reaches an illogical state
+ * (like counter less than zero) it will throw an IllegalStateException.
+ * </p>
+ * <p>
+ * This class can then be used to wrap up operations that while in progress should block tests from
+ * accessing the UI.
+ * </p>
+ *
+ * <pre>
+ * {@code
+ *   public interface FooServer {
+ *     public Foo newFoo();
+ *     public void updateFoo(Foo foo);
+ *   }
+ *
+ *   public DecoratedFooServer implements FooServer {
+ *     private final FooServer realFooServer;
+ *     private final CountingIdlingResource fooServerIdlingResource;
+ *
+ *     public DecoratedFooServer(FooServer realFooServer,
+ *         CountingIdlingResource fooServerIdlingResource) {
+ *       this.realFooServer = checkNotNull(realFooServer);
+ *       this.fooServerIdlingResource = checkNotNull(fooServerIdlingResource);
+ *     }
+ *
+ *     public Foo newFoo() {
+ *       fooServerIdlingResource.increment();
+ *       try {
+ *         return realFooServer.newFoo();
+ *       } finally {
+ *         fooServerIdlingResource.decrement();
+ *       }
+ *     }
+ *
+ *     public void updateFoo(Foo foo) {
+ *       fooServerIdlingResource.increment();
+ *       try {
+ *         realFooServer.updateFoo(foo);
+ *       } finally {
+ *         fooServerIdlingResource.decrement();
+ *       }
+ *     }
+ *   }
+ *   }
+ *   </pre>
+ *
+ *   Then in your test setup:
+ *   <pre>
+ *   {@code
+ *     public void setUp() throws Exception {
+ *       super.setUp();
+ *       FooServer realServer = FooApplication.getFooServer();
+ *       CountingIdlingResource countingResource = new CountingIdlingResource("FooServerCalls");
+ *       FooApplication.setFooServer(new DecoratedFooServer(realServer, countingResource));
+ *       Espresso.registerIdlingResource(countingResource);
+ *     }
+ *   }
+ *   </pre>
+ *
+ */
+@SuppressWarnings("javadoc")
+public final class CountingIdlingResource implements IdlingResource {
+  private static final String TAG = "CountingIdlingResource";
+  private final String resourceName;
+  private final AtomicInteger counter = new AtomicInteger(0);
+  private final boolean debugCounting;
+
+  // written from main thread, read from any thread.
+  private volatile ResourceCallback resourceCallback;
+
+  // read/written from any thread - used for debugging messages.
+  private volatile long becameBusyAt = 0;
+  private volatile long becameIdleAt = 0;
+
+  /**
+   * Creates a CountingIdlingResource without debug tracing.
+   *
+   * @param resourceName the resource name this resource should report to Espresso.
+   */
+  public CountingIdlingResource(String resourceName) {
+    this(resourceName, false);
+  }
+
+  /**
+   * Creates a CountingIdlingResource.
+   *
+   * @param resourceName the resource name this resource should report to Espresso.
+   * @param debugCounting if true increment & decrement calls will print trace information to logs.
+   */
+  public CountingIdlingResource(String resourceName, boolean debugCounting) {
+    this.resourceName = checkNotNull(resourceName);
+    this.debugCounting = debugCounting;
+  }
+
+  @Override
+  public String getName() {
+    return resourceName;
+  }
+
+  @Override
+  public boolean isIdleNow() {
+    return counter.get() == 0;
+  }
+
+  @Override
+  public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {
+    this.resourceCallback = resourceCallback;
+  }
+
+  /**
+   * Increments the count of in-flight transactions to the resource being monitored.
+   *
+   * This method can be called from any thread.
+   */
+  public void increment() {
+    int counterVal = counter.getAndIncrement();
+    if (0 == counterVal) {
+      becameBusyAt = SystemClock.uptimeMillis();
+    }
+
+    if (debugCounting) {
+      Log.i(TAG, "Resource: " + resourceName + " in-use-count incremented to: " + (counterVal + 1));
+    }
+  }
+
+  /**
+   * Decrements the count of in-flight transactions to the resource being monitored.
+   *
+   * If this operation results in the counter falling below 0 - an exception is raised.
+   *
+   * @throws IllegalStateException if the counter is below 0.
+   */
+  public void decrement() {
+    int counterVal = counter.decrementAndGet();
+
+    if (counterVal == 0) {
+      // we've gone from non-zero to zero. That means we're idle now! Tell espresso.
+      if (null != resourceCallback) {
+        resourceCallback.onTransitionToIdle();
+      }
+      becameIdleAt = SystemClock.uptimeMillis();
+    }
+
+    if (debugCounting) {
+      if (counterVal == 0) {
+        Log.i(TAG, "Resource: " + resourceName + " went idle! (Time spent not idle: " +
+            (becameIdleAt - becameBusyAt) + ")");
+      } else {
+        Log.i(TAG, "Resource: " + resourceName + " in-use-count decremented to: " + counterVal);
+      }
+    }
+    checkState(counterVal > -1, "Counter has been corrupted!");
+  }
+
+  /**
+   * Prints the current state of this resource to the logcat at info level.
+   */
+  public void dumpStateToLogs() {
+    StringBuilder message = new StringBuilder("Resource: ")
+        .append(resourceName)
+        .append(" inflight transaction count: ")
+        .append(counter.get());
+    if (0 == becameBusyAt) {
+      Log.i(TAG, message.append(" and has never been busy!").toString());
+    } else {
+      message.append(" and was last busy at: ")
+          .append(becameBusyAt);
+      if (0 == becameIdleAt) {
+        Log.w(TAG, message.append(" AND NEVER WENT IDLE!").toString());
+      } else {
+        message.append(" and last went idle at: ")
+            .append(becameIdleAt);
+        Log.i(TAG, message.toString());
+      }
+    }
+  }
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/matcher/BoundedMatcher.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/matcher/BoundedMatcher.java
new file mode 100644
index 0000000..55e8dde
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/matcher/BoundedMatcher.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.matcher;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import org.hamcrest.BaseMatcher;
+
+/**
+ * Some matcher sugar that lets you create a matcher for a given type
+ * but only process items of a specific subtype of that matcher.
+ *
+ * @param <T> The desired type of the Matcher.
+ * @param <S> the subtype of T that your matcher applies safely to.
+ */
+public abstract class BoundedMatcher<T, S extends T> extends BaseMatcher<T> {
+
+  private final Class<?> expectedType;
+  private final Class<?>[] interfaceTypes;
+
+  public BoundedMatcher(Class<? extends S> expectedType) {
+    this.expectedType = checkNotNull(expectedType);
+    this.interfaceTypes = new Class[0];
+  }
+
+  public BoundedMatcher(Class<?> expectedType, Class<?> interfaceType1,
+      Class<?>... otherInterfaces) {
+    this.expectedType = checkNotNull(expectedType);
+    checkNotNull(otherInterfaces);
+    int interfaceCount = otherInterfaces.length + 1;
+    this.interfaceTypes = new Class[interfaceCount];
+
+    interfaceTypes[0] = checkNotNull(interfaceType1);
+    checkArgument(interfaceType1.isInterface());
+    int interfaceTypeIdx = 1;
+    for (Class<?> intfType : otherInterfaces) {
+      interfaceTypes[interfaceTypeIdx] = checkNotNull(intfType);
+      checkArgument(intfType.isInterface());
+      interfaceTypeIdx++;
+    }
+  }
+
+  protected abstract boolean matchesSafely(S item);
+
+  @Override
+  @SuppressWarnings({"unchecked"})
+  public final boolean matches(Object item) {
+    if (item == null) {
+      return false;
+    }
+
+    if (expectedType.isInstance(item)) {
+      for (Class<?> intfType : interfaceTypes) {
+        if (!intfType.isInstance(item)) {
+          return false;
+        }
+      }
+      return matchesSafely((S) item);
+    }
+    return false;
+  }
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/matcher/PreferenceMatchers.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/matcher/PreferenceMatchers.java
new file mode 100644
index 0000000..13b6506
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/matcher/PreferenceMatchers.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.matcher;
+
+import static org.hamcrest.Matchers.is;
+
+import android.content.res.Resources;
+import android.preference.Preference;
+
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+
+/**
+ * A collection of hamcrest matchers that match {@link Preference}s.
+ */
+public final class PreferenceMatchers {
+
+  private PreferenceMatchers() {}
+
+  public static Matcher<Preference> withSummary(final int resourceId) {
+    return new TypeSafeMatcher<Preference>() {
+      private String resourceName = null;
+      private String expectedText = null;
+
+      @Override
+      public void describeTo(Description description) {
+        description.appendText(" with summary string from resource id: ");
+        description.appendValue(resourceId);
+        if (null != resourceName) {
+          description.appendText("[");
+          description.appendText(resourceName);
+          description.appendText("]");
+        }
+        if (null != expectedText) {
+          description.appendText(" value: " );
+          description.appendText(expectedText);
+        }
+      }
+
+      @Override
+      public boolean matchesSafely(Preference preference) {
+        if (null == expectedText) {
+          try {
+            expectedText = preference.getContext().getResources().getString(resourceId);
+            resourceName = preference.getContext().getResources().getResourceEntryName(resourceId);
+          } catch (Resources.NotFoundException ignored) {
+            /* view could be from a context unaware of the resource id. */
+          }
+        }
+        if (null != expectedText) {
+          return expectedText.equals(preference.getSummary().toString());
+        } else {
+          return false;
+        }
+      }
+    };
+  }
+
+  public static Matcher<Preference> withSummaryText(String summary) {
+    return withSummaryText(is(summary));
+  }
+
+  public static Matcher<Preference> withSummaryText(final Matcher<String> summaryMatcher) {
+    return new TypeSafeMatcher<Preference>() {
+      @Override
+      public void describeTo(Description description) {
+        description.appendText(" a preference with summary matching: ");
+        summaryMatcher.describeTo(description);
+      }
+
+      @Override
+      public boolean matchesSafely(Preference pref) {
+        String summary = pref.getSummary().toString();
+        return summaryMatcher.matches(summary);
+      }
+    };
+  }
+
+  public static Matcher<Preference> withTitle(final int resourceId) {
+    return new TypeSafeMatcher<Preference>() {
+      private String resourceName = null;
+      private String expectedText = null;
+
+      @Override
+      public void describeTo(Description description) {
+        description.appendText(" with title string from resource id: ");
+        description.appendValue(resourceId);
+        if (null != resourceName) {
+          description.appendText("[");
+          description.appendText(resourceName);
+          description.appendText("]");
+        }
+        if (null != expectedText) {
+          description.appendText(" value: " );
+          description.appendText(expectedText);
+        }
+      }
+
+      @Override
+      public boolean matchesSafely(Preference preference) {
+        if (null == expectedText) {
+          try {
+            expectedText = preference.getContext().getResources().getString(resourceId);
+            resourceName = preference.getContext().getResources().getResourceEntryName(resourceId);
+          } catch (Resources.NotFoundException ignored) {
+            /* view could be from a context unaware of the resource id. */
+          }
+        }
+        if (null != expectedText) {
+          return expectedText.equals(preference.getTitle().toString());
+        } else {
+          return false;
+        }
+      }
+    };
+  }
+
+  public static Matcher<Preference> withTitleText(String title) {
+    return withTitleText(is(title));
+  }
+
+  public static Matcher<Preference> withTitleText(final Matcher<String> titleMatcher) {
+    return new TypeSafeMatcher<Preference>() {
+      @Override
+      public void describeTo(Description description) {
+        description.appendText(" a preference with title matching: ");
+        titleMatcher.describeTo(description);
+      }
+
+      @Override
+      public boolean matchesSafely(Preference pref) {
+        String title = pref.getTitle().toString();
+        return titleMatcher.matches(title);
+      }
+    };
+  }
+
+  public static Matcher<Preference> isEnabled() {
+    return new TypeSafeMatcher<Preference>() {
+      @Override
+      public void describeTo(Description description) {
+        description.appendText(" is an enabled preference");
+      }
+
+      @Override
+      public boolean matchesSafely(Preference pref) {
+        return pref.isEnabled();
+      }
+    };
+  }
+
+  public static Matcher<Preference> withKey(String key) {
+    return withKey(is(key));
+  }
+
+  public static Matcher<Preference> withKey(final Matcher<String> keyMatcher) {
+    return new TypeSafeMatcher<Preference>() {
+      @Override
+      public void describeTo(Description description) {
+        description.appendText(" preference with key matching: ");
+        keyMatcher.describeTo(description);
+      }
+
+      @Override
+      public boolean matchesSafely(Preference pref) {
+        return keyMatcher.matches(pref.getKey());
+      }
+    };
+  }
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/matcher/RootMatchers.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/matcher/RootMatchers.java
new file mode 100644
index 0000000..03be6c7
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/matcher/RootMatchers.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.matcher;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.anyOf;
+
+import com.google.android.apps.common.testing.testrunner.ActivityLifecycleMonitor;
+import com.google.android.apps.common.testing.testrunner.ActivityLifecycleMonitorRegistry;
+import com.google.android.apps.common.testing.testrunner.Stage;
+import com.google.android.apps.common.testing.ui.espresso.NoActivityResumedException;
+import com.google.android.apps.common.testing.ui.espresso.Root;
+import com.google.common.collect.Lists;
+
+import android.app.Activity;
+import android.os.IBinder;
+import android.view.View;
+import android.view.WindowManager;
+
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * A collection of matchers for {@link Root} objects.
+ */
+public final class RootMatchers {
+
+  private RootMatchers() {}
+
+  /**
+   * Espresso's default {@link Root} matcher.
+   */
+  @SuppressWarnings("unchecked")
+  public static final Matcher<Root> DEFAULT =
+      allOf(
+        hasWindowLayoutParams(),
+        allOf(
+             anyOf(
+                  allOf(isDialog(), withDecorView(hasWindowFocus())),
+                  isSubwindowOfCurrentActivity()),
+             isFocusable()));
+
+
+  /**
+   * Matches {@link Root}s that can take window focus.
+   */
+  public static Matcher<Root> isFocusable() {
+    return new TypeSafeMatcher<Root>() {
+
+      @Override
+      public void describeTo(Description description) {
+        description.appendText("is focusable");
+      }
+
+      @Override
+      public boolean matchesSafely(Root root) {
+        int flags = root.getWindowLayoutParams().get().flags;
+        boolean r = !((flags & WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE) != 0);
+        return r;
+      }
+    };
+  }
+
+  /**
+   * Matches {@link Root}s that can receive touch events.
+   */
+  public static Matcher<Root> isTouchable() {
+    return new TypeSafeMatcher<Root>() {
+
+      @Override
+      public void describeTo(Description description) {
+        description.appendText("is touchable");
+      }
+
+      @Override
+      public boolean matchesSafely(Root root) {
+        int flags = root.getWindowLayoutParams().get().flags;
+        boolean r = !((flags & WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE) != 0);
+        return r;
+      }
+    };
+  }
+
+  /**
+   * Matches {@link Root}s that are dialogs (i.e. is not a window of the currently resumed
+   * activity).
+   */
+  public static Matcher<Root> isDialog() {
+    return new TypeSafeMatcher<Root>() {
+
+      @Override
+      public void describeTo(Description description) {
+        description.appendText("is dialog");
+      }
+
+      @Override
+      public boolean matchesSafely(Root root) {
+        int type = root.getWindowLayoutParams().get().type;
+        if ((type != WindowManager.LayoutParams.TYPE_BASE_APPLICATION
+            && type < WindowManager.LayoutParams.LAST_APPLICATION_WINDOW)) {
+          IBinder windowToken = root.getDecorView().getWindowToken();
+          IBinder appToken = root.getDecorView().getApplicationWindowToken();
+          if (windowToken == appToken) {
+            // windowToken == appToken means this window isn't contained by any other windows.
+            // if it was a window for an activity, it would have TYPE_BASE_APPLICATION.
+            // therefore it must be a dialog box.
+            return true;
+          }
+        }
+        return false;
+      }
+    };
+  }
+
+  /**
+   * Matches {@link Root}s with decor views that match the given view matcher.
+   */
+  public static Matcher<Root> withDecorView(final Matcher<View> decorViewMatcher) {
+    checkNotNull(decorViewMatcher);
+    return new TypeSafeMatcher<Root>() {
+
+      @Override
+      public void describeTo(Description description) {
+        description.appendText("with decor view ");
+        decorViewMatcher.describeTo(description);
+      }
+
+      @Override
+      public boolean matchesSafely(Root root) {
+        return decorViewMatcher.matches(root.getDecorView());
+      }
+    };
+  }
+
+  private static Matcher<View> hasWindowFocus() {
+    return new TypeSafeMatcher<View>() {
+      @Override
+      public void describeTo(Description description) {
+        description.appendText("has window focus");
+      }
+
+      @Override
+      public boolean matchesSafely(View view) {
+        return view.hasWindowFocus();
+      }
+    };
+  }
+
+  private static Matcher<Root> hasWindowLayoutParams() {
+    return new TypeSafeMatcher<Root>() {
+
+      @Override
+      public void describeTo(Description description) {
+        description.appendText("has window layout params");
+      }
+
+      @Override
+      public boolean matchesSafely(Root root) {
+        if (!root.getWindowLayoutParams().isPresent()) {
+          return false;
+        }
+        return true;
+      }
+    };
+  }
+
+  private static Matcher<Root> isSubwindowOfCurrentActivity() {
+    return new TypeSafeMatcher<Root>() {
+
+      @Override
+      public void describeTo(Description description) {
+        description.appendText("is subwindow of current activity");
+      }
+
+      @Override
+      public boolean matchesSafely(Root root) {
+        boolean r =
+            getResumedActivityTokens().contains(root.getDecorView().getApplicationWindowToken());
+        return r;
+      }
+    };
+  }
+
+  private static List<IBinder> getResumedActivityTokens() {
+    ActivityLifecycleMonitor activityLifecycleMonitor =
+        ActivityLifecycleMonitorRegistry.getInstance();
+    Collection<Activity> resumedActivities =
+        activityLifecycleMonitor.getActivitiesInStage(Stage.RESUMED);
+    if (resumedActivities.isEmpty()) {
+      throw new NoActivityResumedException("At least one activity should be in RESUMED stage.");
+    }
+    List<IBinder> tokens = Lists.newArrayList();
+    for (Activity activity : resumedActivities) {
+      tokens.add(activity.getWindow().getDecorView().getApplicationWindowToken());
+    }
+    return tokens;
+  }
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/matcher/ViewMatchers.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/matcher/ViewMatchers.java
new file mode 100644
index 0000000..286f494
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/matcher/ViewMatchers.java
@@ -0,0 +1,809 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.matcher;
+
+import static com.google.android.apps.common.testing.ui.espresso.util.TreeIterables.breadthFirstViewTraversal;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import static org.hamcrest.Matchers.is;
+
+import com.google.android.apps.common.testing.ui.espresso.util.HumanReadables;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.widget.Checkable;
+import android.widget.TextView;
+
+import junit.framework.AssertionFailedError;
+
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.Matchers;
+import org.hamcrest.StringDescription;
+import org.hamcrest.TypeSafeMatcher;
+
+import java.util.Iterator;
+
+/**
+ * A collection of hamcrest matchers that match {@link View}s.
+ */
+public final class ViewMatchers {
+
+  private ViewMatchers() {}
+
+  /**
+   * Returns a matcher that matches Views which are an instance of or subclass of the provided
+   * class. Some versions of Hamcrest make the generic typing of this a nightmare, so we have a
+   * special case for our users.
+   */
+  public static Matcher<View> isAssignableFrom(final Class<? extends View> clazz) {
+    checkNotNull(clazz);
+    return new TypeSafeMatcher<View>() {
+      @Override
+      public void describeTo(Description description) {
+        description.appendText("is assignable from class: " + clazz);
+      }
+
+      @Override
+      public boolean matchesSafely(View view) {
+        return clazz.isAssignableFrom(view.getClass());
+      }
+    };
+  }
+
+ /**
+   * Returns a matcher that matches Views with class name matching the given matcher.
+   */
+  public static Matcher<View> withClassName(final Matcher<String> classNameMatcher) {
+    checkNotNull(classNameMatcher);
+    return new TypeSafeMatcher<View>() {
+      @Override
+      public void describeTo(Description description) {
+        description.appendText("with class name: ");
+        classNameMatcher.describeTo(description);
+      }
+
+      @Override
+      public boolean matchesSafely(View view) {
+        return classNameMatcher.matches(view.getClass().getName());
+      }
+    };
+  }
+
+  /**
+   * Returns a matcher that matches {@link View}s that are currently displayed on the screen to the
+   * user.
+   *
+   * Note: isDisplayed will select views that are partially displayed (eg: the full height/width of
+   * the view is greater then the height/width of the visible rectangle). If you wish to ensure the
+   * entire rectangle this view draws is displayed to the user use isCompletelyDisplayed.
+   */
+  public static Matcher<View> isDisplayed() {
+    return new TypeSafeMatcher<View>() {
+      @Override
+      public void describeTo(Description description) {
+        description.appendText("is displayed on the screen to the user");
+      }
+
+      @Override
+      public boolean matchesSafely(View view) {
+        return view.getGlobalVisibleRect(new Rect()) &&
+            withEffectiveVisibility(Visibility.VISIBLE).matches(view);
+      }
+    };
+  }
+
+  /**
+   * Returns a matcher which only accepts a view whose height and width fit perfectly within
+   * the currently displayed region of this view.
+   *
+   * There exist views (such as ScrollViews) whose height and width are larger then the physical
+   * device screen by design. Such views will _never_ be completely displayed.
+   */
+  public static Matcher<View> isCompletelyDisplayed() {
+    return isDisplayingAtLeast(100);
+  }
+
+  /**
+   * Returns a matcher which accepts a view so long as a given percentage of that view's area is
+   * not obscured by any other view and is thus visible to the user.
+   *
+   * @param areaPercentage an integer ranging from (0, 100] indicating how much percent of the
+   *   surface area of the view must be shown to the user to be accepted.
+   */
+  public static Matcher<View> isDisplayingAtLeast(final int areaPercentage) {
+    checkState(areaPercentage <= 100, "Cannot have over 100 percent: %s", areaPercentage);
+    checkState(areaPercentage > 0, "Must have a positive, non-zero value: %s", areaPercentage);
+    return new TypeSafeMatcher<View>() {
+      @Override
+      public void describeTo(Description description) {
+        description.appendText(String.format(
+            "at least %s percent of the view's area is displayed to the user.", areaPercentage));
+      }
+
+      @Override
+      public boolean matchesSafely(View view) {
+        Rect visibleParts = new Rect();
+        boolean visibleAtAll = view.getGlobalVisibleRect(visibleParts);
+        if (!visibleAtAll) {
+          return false;
+        }
+        double maxArea = view.getHeight() * view.getWidth();
+        double visibleArea = visibleParts.height() * visibleParts.width();
+        int displayedPercentage = (int) ((visibleArea / maxArea) * 100);
+
+        return displayedPercentage >= areaPercentage
+            && withEffectiveVisibility(Visibility.VISIBLE).matches(view);
+      }
+    };
+  }
+
+
+
+  /**
+   * Returns a matcher that matches {@link View}s that are enabled.
+   */
+  public static Matcher<View> isEnabled() {
+    return new TypeSafeMatcher<View>() {
+      @Override
+      public void describeTo(Description description) {
+        description.appendText("is enabled");
+      }
+
+      @Override
+      public boolean matchesSafely(View view) {
+        return view.isEnabled();
+      }
+    };
+  }
+
+  /**
+   * Returns a matcher that matches {@link View}s that are focusable.
+   */
+  public static Matcher<View> isFocusable() {
+    return new TypeSafeMatcher<View>() {
+      @Override
+      public void describeTo(Description description) {
+        description.appendText("is focusable");
+      }
+
+      @Override
+      public boolean matchesSafely(View view) {
+        return view.isFocusable();
+      }
+    };
+  }
+
+  /**
+   * Returns a matcher that matches {@link View}s currently have focus.
+   */
+  public static Matcher<View> hasFocus() {
+    return new TypeSafeMatcher<View>() {
+      @Override
+      public void describeTo(Description description) {
+        description.appendText("has focus on the screen to the user");
+      }
+
+      @Override
+      public boolean matchesSafely(View view) {
+        return view.hasFocus();
+      }
+    };
+  }
+
+  /**
+   * Returns an {@link Matcher} that matches {@link View}s based on their siblings.<br>
+   * <br>
+   * This may be particularly useful when a view cannot be uniquely selected on properties such as
+   * text or R.id. For example: a call button is repeated several times in a contacts layout and the
+   * only way to differentiate the call button view is by what appears next to it (e.g. the unique
+   * name of the contact).
+   *
+   * @param siblingMatcher a {@link Matcher} for the sibling of the view.
+   */
+  public static Matcher<View> hasSibling(final Matcher<View> siblingMatcher) {
+    checkNotNull(siblingMatcher);
+    return new TypeSafeMatcher<View>() {
+      @Override
+      public void describeTo(Description description) {
+        description.appendText("has sibling: ");
+        siblingMatcher.describeTo(description);
+      }
+
+      @Override
+      public boolean matchesSafely(View view) {
+        ViewParent parent = view.getParent();
+        if (!(parent instanceof ViewGroup)) {
+          return false;
+        }
+        ViewGroup parentGroup = (ViewGroup) parent;
+        for (int i = 0; i < parentGroup.getChildCount(); i++) {
+          if (siblingMatcher.matches(parentGroup.getChildAt(i))) {
+            return true;
+          }
+        }
+        return false;
+      }
+    };
+  }
+
+  /**
+   * Returns an {@link Matcher} that matches {@link View}s based on content description property
+   * value. Sugar for withContentDescription(is("string")).
+   *
+   * @param text the text to match on.
+   */
+  public static Matcher<View> withContentDescription(String text) {
+    return withContentDescription(is(text));
+  }
+
+  /**
+   * Returns an {@link Matcher} that matches {@link View}s based on content description property
+   * value.
+   *
+   * @param charSequenceMatcher a {@link CharSequence} {@link Matcher} for the content description
+   */
+  public static Matcher<View> withContentDescription(
+      final Matcher<? extends CharSequence> charSequenceMatcher) {
+    checkNotNull(charSequenceMatcher);
+    return new TypeSafeMatcher<View>() {
+      @Override
+      public void describeTo(Description description) {
+        description.appendText("with content description: ");
+        charSequenceMatcher.describeTo(description);
+      }
+
+      @Override
+      public boolean matchesSafely(View view) {
+        return charSequenceMatcher.matches(view.getContentDescription());
+      }
+    };
+  }
+
+  /**
+   * Sugar for withId(is(int)).
+   *
+   * @param id the resource id.
+   */
+  public static Matcher<View> withId(int id) {
+    return withId(is(id));
+  }
+
+  /**
+   * Returns a matcher that matches {@link View}s based on resource ids. Note: Android resource ids
+   * are not guaranteed to be unique. You may have to pair this matcher with another one to
+   * guarantee a unique view selection.
+   *
+   * @param integerMatcher a Matcher for resource ids
+   */
+  public static Matcher<View> withId(final Matcher<Integer> integerMatcher) {
+    checkNotNull(integerMatcher);
+    return new TypeSafeMatcher<View>() {
+      @Override
+      public void describeTo(Description description) {
+        description.appendText("with id: ");
+        integerMatcher.describeTo(description);
+      }
+
+      @Override
+      public boolean matchesSafely(View view) {
+        return integerMatcher.matches(view.getId());
+      }
+    };
+  }
+
+  /**
+   * Returns a matcher that matches {@link View} based on tag keys.
+   *
+   * @param key to match
+   */
+  public static Matcher<View> withTagKey(final int key) {
+    return withTagKey(key, Matchers.<Object>notNullValue());
+  }
+
+  /**
+   * Returns a matcher that matches {@link View}s based on tag keys.
+   *
+   * @param key to match
+   * @param objectMatcher Object to match
+   */
+  public static Matcher<View> withTagKey(final int key, final Matcher<Object> objectMatcher) {
+    checkNotNull(objectMatcher);
+    return new TypeSafeMatcher<View>() {
+      @Override
+      public void describeTo(Description description) {
+        description.appendText("with key: " + key);
+        objectMatcher.describeTo(description);
+      }
+
+      @Override
+      public boolean matchesSafely(View view) {
+        return objectMatcher.matches(view.getTag(key));
+      }
+    };
+  }
+
+  /**
+   * Returns a matcher that matches {@link View}s based on tag property values.
+   *
+   * @param tagValueMatcher a Matcher for the view's tag property value
+   */
+  public static Matcher<View> withTagValue(final Matcher<Object> tagValueMatcher) {
+    checkNotNull(tagValueMatcher);
+    return new TypeSafeMatcher<View>() {
+      @Override
+      public void describeTo(Description description) {
+        description.appendText("with tag value: ");
+        tagValueMatcher.describeTo(description);
+      }
+
+      @Override
+      public boolean matchesSafely(View view) {
+        return tagValueMatcher.matches(view.getTag());
+      }
+    };
+  }
+
+  /**
+   * Returns a matcher that matches {@link TextView} based on it's text property value. Note: View's
+   * Sugar for withText(is("string")).
+   */
+  public static Matcher<View> withText(String text) {
+    return withText(is(text));
+  }
+
+  /**
+   * Returns a matcher that matches {@link TextView}s based on text property value. Note: View's
+   * text property is never null. If you setText(null) it will still be "". Do not use null matcher.
+   *
+   * @param stringMatcher {@link Matcher} of {@link String} with text to match
+   */
+  public static Matcher<View> withText(final Matcher<String> stringMatcher) {
+    checkNotNull(stringMatcher);
+    return new BoundedMatcher<View, TextView>(TextView.class) {
+      @Override
+      public void describeTo(Description description) {
+        description.appendText("with text: ");
+        stringMatcher.describeTo(description);
+      }
+
+      @Override
+      public boolean matchesSafely(TextView textView) {
+        return stringMatcher.matches(textView.getText().toString());
+      }
+    };
+  }
+
+  /**
+   * Returns a matcher that matches a descendant of {@link TextView} that is displaying the string
+   * associated with the given resource id.
+   *
+   * @param resourceId the string resource the text view is expected to hold.
+   */
+  public static Matcher<View> withText(final int resourceId) {
+
+    return new BoundedMatcher<View, TextView>(TextView.class) {
+      private String resourceName = null;
+      private String expectedText = null;
+
+      @Override
+      public void describeTo(Description description) {
+        description.appendText("with string from resource id: ");
+        description.appendValue(resourceId);
+        if (null != resourceName) {
+          description.appendText("[");
+          description.appendText(resourceName);
+          description.appendText("]");
+        }
+        if (null != expectedText) {
+          description.appendText(" value: ");
+          description.appendText(expectedText);
+        }
+      }
+
+      @Override
+      public boolean matchesSafely(TextView textView) {
+        if (null == expectedText) {
+          try {
+            expectedText = textView.getResources().getString(resourceId);
+            resourceName = textView.getResources().getResourceEntryName(resourceId);
+          } catch (Resources.NotFoundException ignored) {
+            /* view could be from a context unaware of the resource id. */
+          }
+        }
+        if (null != expectedText) {
+          return expectedText.equals(textView.getText());
+        } else {
+          return false;
+        }
+      }
+    };
+  }
+
+  /**
+   * Returns a matcher that accepts if and only if the view is a CompoundButton (or subtype of) and
+   * is in checked state.
+   */
+  public static Matcher<View> isChecked() {
+    return withCheckBoxState(is(true));
+  }
+
+  /**
+   * Returns a matcher that accepts if and only if the view is a CompoundButton (or subtype of) and
+   * is not in checked state.
+   */
+  public static Matcher<View> isNotChecked() {
+    return withCheckBoxState(is(false));
+  }
+
+  private static <E extends View & Checkable> Matcher<View> withCheckBoxState(
+      final Matcher<Boolean> checkStateMatcher) {
+
+    return new BoundedMatcher<View, E>(View.class, Checkable.class) {
+      @Override
+      public void describeTo(Description description) {
+        description.appendText("with checkbox state: ");
+        checkStateMatcher.describeTo(description);
+      }
+
+      @Override
+      public boolean matchesSafely(E checkable) {
+        return checkStateMatcher.matches(checkable.isChecked());
+      }
+    };
+  }
+
+  /**
+   * Returns an {@link Matcher} that matches {@link View}s with any content description.
+   */
+  public static Matcher<View> hasContentDescription() {
+    return new TypeSafeMatcher<View>() {
+      @Override
+      public void describeTo(Description description) {
+        description.appendText("has content description");
+      }
+
+      @Override
+      public boolean matchesSafely(View view) {
+        return view.getContentDescription() != null;
+      }
+    };
+  }
+
+  /**
+   * Returns a matcher that matches {@link View}s based on the presence of a descendant in its view
+   * hierarchy.
+   *
+   * @param descendantMatcher the type of the descendant to match on
+   */
+  public static Matcher<View> hasDescendant(final Matcher<View> descendantMatcher) {
+    checkNotNull(descendantMatcher);
+    return new TypeSafeMatcher<View>() {
+      @Override
+      public void describeTo(Description description) {
+        description.appendText("has descendant: ");
+        descendantMatcher.describeTo(description);
+      }
+
+      @Override
+      public boolean matchesSafely(final View view) {
+        final Predicate<View> matcherPredicate = new Predicate<View>() {
+          @Override
+          public boolean apply(View input) {
+            return input != view && descendantMatcher.matches(input);
+          }
+        };
+
+        Iterator<View> matchedViewIterator =
+            Iterables.filter(breadthFirstViewTraversal(view), matcherPredicate).iterator();
+
+        return matchedViewIterator.hasNext();
+      }
+    };
+  }
+
+  /**
+   * Returns a matcher that matches {@link View}s that are clickable.
+   */
+  public static Matcher<View> isClickable() {
+    return new TypeSafeMatcher<View>() {
+      @Override
+      public void describeTo(Description description) {
+        description.appendText("is clickable");
+      }
+
+      @Override
+      public boolean matchesSafely(View view) {
+        return view.isClickable();
+      }
+    };
+  }
+
+  /**
+   * Returns a matcher that matches {@link View}s based on the given ancestor type.
+   *
+   * @param ancestorMatcher the type of the ancestor to match on
+   */
+  public static Matcher<View> isDescendantOfA(final Matcher<View> ancestorMatcher) {
+    checkNotNull(ancestorMatcher);
+    return new TypeSafeMatcher<View>() {
+      @Override
+      public void describeTo(Description description) {
+        description.appendText("is descendant of a: ");
+        ancestorMatcher.describeTo(description);
+      }
+
+      @Override
+      public boolean matchesSafely(View view) {
+        return checkAncestors(view.getParent(), ancestorMatcher);
+      }
+
+      private boolean checkAncestors(
+        ViewParent viewParent, Matcher<View> ancestorMatcher) {
+        if (!(viewParent instanceof View)) {
+          return false;
+        }
+        if (ancestorMatcher.matches(viewParent)) {
+          return true;
+        }
+        return checkAncestors(viewParent.getParent(), ancestorMatcher);
+      }
+    };
+  }
+
+  /**
+   * Returns a matcher that matches {@link View}s that have "effective" visibility set to the given
+   * value. Effective visibility takes into account not only the view's visibility value, but also
+   * that of its ancestors. In case of View.VISIBLE, this means that the view and all of its
+   * ancestors have visibility=VISIBLE. In case of GONE and INVISIBLE, it's the opposite - any GONE
+   * or INVISIBLE parent will make all of its children have their effective visibility.
+   *
+   * <p>
+   * <p>
+   * Note: Contrary to what the name may imply, view visibility does not directly translate into
+   * whether the view is displayed on screen (use isDisplayed() for that). For example, the view and
+   * all of its ancestors can have visibility=VISIBLE, but the view may need to be scrolled to in
+   * order to be actually visible to the user. Unless you're specifically targeting the visibility
+   * value with your test, use isDisplayed.
+   */
+  public static Matcher<View> withEffectiveVisibility(final Visibility visibility) {
+    return new TypeSafeMatcher<View>() {
+      @Override
+      public void describeTo(Description description) {
+        description.appendText(
+            String.format("view has effective visibility=%s", visibility));
+      }
+
+      @Override
+      public boolean matchesSafely(View view) {
+        if (visibility.getValue() == View.VISIBLE) {
+          if (view.getVisibility() != visibility.getValue()) {
+            return false;
+          }
+          while (view.getParent() != null && view.getParent() instanceof View) {
+            view = (View) view.getParent();
+            if (view.getVisibility() != visibility.getValue()) {
+              return false;
+            }
+          }
+          return true;
+        } else {
+          if (view.getVisibility() == visibility.getValue()) {
+            return true;
+          }
+          while (view.getParent() != null && view.getParent() instanceof View) {
+            view = (View) view.getParent();
+            if (view.getVisibility() == visibility.getValue()) {
+              return true;
+            }
+          }
+          return false;
+        }
+      }
+    };
+  }
+
+  /**
+   * Enumerates the possible list of values for View.getVisibility().
+   */
+  public enum Visibility {
+    VISIBLE(View.VISIBLE), INVISIBLE(View.INVISIBLE), GONE(View.GONE);
+
+    private final int value;
+
+    private Visibility(int value) {
+      this.value = value;
+    }
+
+    public int getValue() {
+      return value;
+    }
+  }
+
+   /**
+   * A matcher that accepts a view if and only if the view's parent is accepted by the provided
+   * matcher.
+   *
+   * @param parentMatcher the matcher to apply on getParent.
+   */
+  public static Matcher<View> withParent(final Matcher<View> parentMatcher) {
+    checkNotNull(parentMatcher);
+    return new TypeSafeMatcher<View>() {
+      @Override
+      public void describeTo(Description description) {
+        description.appendText("has parent matching: ");
+        parentMatcher.describeTo(description);
+      }
+
+      @Override
+      public boolean matchesSafely(View view) {
+        return parentMatcher.matches(view.getParent());
+      }
+    };
+  }
+
+   /**
+   * A matcher that returns true if and only if the view's child is accepted by the provided
+   * matcher.
+   *
+   * @param childMatcher the matcher to apply on the child views.
+   */
+  public static Matcher<View> withChild(final Matcher<View> childMatcher) {
+    checkNotNull(childMatcher);
+    return new TypeSafeMatcher<View>() {
+      @Override
+      public void describeTo(Description description) {
+        description.appendText("has child: ");
+        childMatcher.describeTo(description);
+      }
+
+      @Override
+      public boolean matchesSafely(View view) {
+        if (!(view instanceof ViewGroup)) {
+          return false;
+        }
+
+        ViewGroup group = (ViewGroup) view;
+        for (int i = 0; i < group.getChildCount(); i++) {
+          if (childMatcher.matches(group.getChildAt(i))) {
+            return true;
+          }
+        }
+
+        return false;
+      }
+    };
+  }
+
+
+  /**
+   * Returns a matcher that matches root {@link View}.
+   */
+  public static Matcher<View> isRoot() {
+    return new TypeSafeMatcher<View>() {
+      @Override
+      public void describeTo(Description description) {
+        description.appendText("is a root view.");
+      }
+
+      @Override
+      public boolean matchesSafely(View view) {
+        return view.getRootView().equals(view);
+      }
+    };
+  }
+
+  /**
+   * Returns a matcher that matches views that support input methods.
+   */
+  public static Matcher<View> supportsInputMethods() {
+    return new TypeSafeMatcher<View>() {
+      @Override
+      public void describeTo(Description description) {
+        description.appendText("supports input methods");
+      }
+
+      @Override
+      public boolean matchesSafely(View view) {
+        // At first glance, it would make sense to use view.onCheckIsTextEditor, but the android
+        // javadoc is wishy-washy about whether authors are required to implement this method when
+        // implementing onCreateInputConnection.
+        return view.onCreateInputConnection(new EditorInfo()) != null;
+      }
+    };
+  }
+
+  /**
+   * Returns a matcher that matches views that support input methods (e.g. EditText) and have the
+   * specified IME action set in its {@link EditorInfo}.
+   *
+   * @param imeAction the IME action to match
+   */
+  public static Matcher<View> hasImeAction(int imeAction) {
+    return hasImeAction(is(imeAction));
+  }
+
+  /**
+   * Returns a matcher that matches views that support input methods (e.g. EditText) and have the
+   * specified IME action set in its {@link EditorInfo}.
+   *
+   * @param imeActionMatcher a matcher for the IME action
+   */
+  public static Matcher<View> hasImeAction(final Matcher<Integer> imeActionMatcher) {
+    return new TypeSafeMatcher<View>() {
+      @Override
+      public void describeTo(Description description) {
+        description.appendText("has ime action: ");
+        imeActionMatcher.describeTo(description);
+      }
+
+      @Override
+      public boolean matchesSafely(View view) {
+        EditorInfo editorInfo = new EditorInfo();
+        InputConnection inputConnection = view.onCreateInputConnection(editorInfo);
+        if (inputConnection == null) {
+          return false;
+        }
+        int actionId = editorInfo.actionId != 0 ? editorInfo.actionId
+            : editorInfo.imeOptions & EditorInfo.IME_MASK_ACTION;
+        return imeActionMatcher.matches(actionId);
+      }
+    };
+  }
+
+  /**
+   * A replacement for MatcherAssert.assertThat that renders View objects nicely.
+   *
+   * @param actual the actual value.
+   * @param matcher a matcher that accepts or rejects actual.
+   */
+  public static <T> void assertThat(T actual, Matcher<T> matcher) {
+    assertThat("", actual, matcher);
+  }
+
+  /**
+   * A replacement for MatcherAssert.assertThat that renders View objects nicely.
+   *
+   * @param message the message to display.
+   * @param actual the actual value.
+   * @param matcher a matcher that accepts or rejects actual.
+   */
+  public static <T> void assertThat(String message, T actual, Matcher<T> matcher) {
+    if (!matcher.matches(actual)) {
+      Description description = new StringDescription();
+      description.appendText(message)
+          .appendText("\nExpected: ")
+          .appendDescriptionOf(matcher)
+          .appendText("\n     Got: ");
+      if (actual instanceof View) {
+        description.appendValue(HumanReadables.describe((View) actual));
+      } else {
+        description.appendValue(actual);
+      }
+      description.appendText("\n");
+      throw new AssertionFailedError(description.toString());
+    }
+  }
+}
+
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/util/HumanReadables.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/util/HumanReadables.java
new file mode 100644
index 0000000..a160cea
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/util/HumanReadables.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.util;
+
+import static com.google.android.apps.common.testing.ui.espresso.util.TreeIterables.depthFirstViewTraversalWithDistance;
+
+import com.google.android.apps.common.testing.ui.espresso.util.TreeIterables.ViewAndDistance;
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.base.Objects;
+import com.google.common.base.Objects.ToStringHelper;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
+
+import android.content.res.Resources;
+import android.os.Build;
+import android.util.Printer;
+import android.util.StringBuilderPrinter;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.widget.Checkable;
+import android.widget.TextView;
+
+import java.util.List;
+
+/**
+ * Text converters for various Android objects.
+ */
+public final class HumanReadables {
+
+  private HumanReadables() {}
+
+  /**
+   * Prints out an error message feature the view hierarchy starting at the rootView.
+   *
+   * @param rootView the root of the hierarchy tree to print out.
+   * @param problemViews list of the views that you would like to point out are causing the error
+   *        message or null, if you want to skip this feature.
+   * @param errorHeader the header of the error message (should contain the description of why the
+   *        error is happening).
+   * @param problemViewSuffix the message to append to the view description in the tree printout.
+   *        Required if problemViews is supplied. Otherwise, null is acceptable.
+   * @return a string for human consumption.
+   */
+  public static String getViewHierarchyErrorMessage(View rootView,
+      final List<View> problemViews,
+      String errorHeader,
+      final String problemViewSuffix) {
+    Preconditions.checkArgument(problemViews == null || problemViewSuffix != null);
+    StringBuilder errorMessage = new StringBuilder(errorHeader);
+    if (problemViewSuffix != null) {
+      errorMessage.append(
+          String.format("\nProblem views are marked with '%s' below.", problemViewSuffix));
+    }
+
+    errorMessage.append("\n\nView Hierarchy:\n");
+
+    Joiner.on("\n").appendTo(errorMessage, Iterables.transform(
+        depthFirstViewTraversalWithDistance(rootView), new Function<ViewAndDistance, String>() {
+          @Override
+          public String apply(ViewAndDistance viewAndDistance) {
+            String formatString = "+%s%s ";
+            if (problemViews != null
+                && problemViews.contains(viewAndDistance.getView())) {
+              formatString += problemViewSuffix;
+            }
+            formatString += "\n|";
+
+            return String.format(formatString,
+                Strings.padStart(">", viewAndDistance.getDistanceFromRoot() + 1, '-'),
+                HumanReadables.describe(viewAndDistance.getView()));
+          }
+        }));
+
+    return errorMessage.toString();
+  }
+
+  /**
+   * Transforms an arbitrary view into a string with (hopefully) enough debug info.
+   *
+   * @param v nullable view
+   * @return a string for human consumption.
+   */
+  public static String describe(View v) {
+    if (null == v) {
+      return "null";
+    }
+    ToStringHelper helper = Objects.toStringHelper(v).add("id", v.getId());
+    if (v.getId() != -1 && v.getResources() != null) {
+      try {
+        helper.add("res-name", v.getResources().getResourceEntryName(v.getId()));
+      } catch (Resources.NotFoundException ignore) {
+        // Do nothing.
+      }
+    }
+    if (null != v.getContentDescription()) {
+      helper.add("desc", v.getContentDescription());
+    }
+
+    switch (v.getVisibility()) {
+      case View.GONE:
+        helper.add("visibility", "GONE");
+        break;
+      case View.INVISIBLE:
+        helper.add("visibility", "INVISIBLE");
+        break;
+      case View.VISIBLE:
+        helper.add("visibility", "VISIBLE");
+        break;
+      default:
+        helper.add("visibility", v.getVisibility());
+    }
+
+    helper.add("width", v.getWidth())
+      .add("height", v.getHeight())
+      .add("has-focus", v.hasFocus())
+      .add("has-focusable", v.hasFocusable())
+      .add("has-window-focus", v.hasWindowFocus())
+      .add("is-clickable", v.isClickable())
+      .add("is-enabled", v.isEnabled())
+      .add("is-focused", v.isFocused())
+      .add("is-focusable", v.isFocusable())
+      .add("is-layout-requested", v.isLayoutRequested())
+      .add("is-selected", v.isSelected());
+
+    if (null != v.getRootView()) {
+      // pretty much only true in unit-tests.
+      helper.add("root-is-layout-requested", v.getRootView().isLayoutRequested());
+    }
+
+    EditorInfo ei = new EditorInfo();
+    InputConnection ic = v.onCreateInputConnection(ei);
+    boolean hasInputConnection = ic != null;
+    helper.add("has-input-connection", hasInputConnection);
+    if (hasInputConnection) {
+      StringBuilder sb = new StringBuilder();
+      sb.append("[");
+      Printer p = new StringBuilderPrinter(sb);
+      ei.dump(p, "");
+      sb.append("]");
+      helper.add("editor-info", sb.toString().replace("\n", " "));
+    }
+
+    if (Build.VERSION.SDK_INT > 10) {
+      helper.add("x", v.getX()).add("y", v.getY());
+    }
+
+    if (v instanceof TextView) {
+      innerDescribe((TextView) v, helper);
+    }
+    if (v instanceof Checkable) {
+      innerDescribe((Checkable) v, helper);
+    }
+    if (v instanceof ViewGroup) {
+      innerDescribe((ViewGroup) v, helper);
+    }
+    return helper.toString();
+  }
+
+  private static void innerDescribe(TextView textBox, ToStringHelper helper) {
+    if (null != textBox.getText()) {
+      helper.add("text", textBox.getText());
+    }
+
+    if (null != textBox.getError()) {
+      helper.add("error-text", textBox.getError());
+    }
+
+    if (null != textBox.getHint()) {
+      helper.add("hint", textBox.getHint());
+    }
+
+    helper.add("input-type", textBox.getInputType());
+    helper.add("ime-target", textBox.isInputMethodTarget());
+  }
+
+  private static void innerDescribe(Checkable checkable, ToStringHelper helper) {
+    helper.add("is-checked", checkable.isChecked());
+  }
+
+  private static void innerDescribe(ViewGroup viewGroup, ToStringHelper helper) {
+    helper.add("child-count", viewGroup.getChildCount());
+  }
+}
diff --git a/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/util/TreeIterables.java b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/util/TreeIterables.java
new file mode 100644
index 0000000..7fd0c4f
--- /dev/null
+++ b/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/util/TreeIterables.java
@@ -0,0 +1,300 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso.util;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.collect.AbstractIterator;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Utility methods for iterating over tree structured items.
+ *
+ * Since the view hierarchy is a tree - having a method of iterating over its contents
+ * is useful.
+ *
+ * This is generalized for any object which can display tree like qualities - but this
+ * generalization was done for testability concerns (since creating View hierarchies is a pain).
+ *
+ * Only public methods of this utility class are considered public API of the test framework.
+ */
+public final class TreeIterables {
+  private static final TreeViewer<View> VIEW_TREE_VIEWER = new ViewTreeViewer();
+
+  private TreeIterables() { }
+
+  /**
+   * Creates an iterable that traverses the tree formed by the given root.
+   *
+   * Along with iteration order, the distance from the root element is also tracked.
+   *
+   * @param root the root view to track from.
+   * @return An iterable of ViewAndDistance containing the view tree in a depth first order with
+   *   the distance of a given node from the root.
+   */
+  public static Iterable<ViewAndDistance> depthFirstViewTraversalWithDistance(View root) {
+    final DistanceRecordingTreeViewer<View> distanceRecorder =
+        new DistanceRecordingTreeViewer<View>(root, VIEW_TREE_VIEWER);
+
+
+    return Iterables.transform(
+        depthFirstTraversal(root, distanceRecorder),
+        new Function<View, ViewAndDistance>() {
+          @Override
+          public ViewAndDistance apply(View view) {
+            return new ViewAndDistance(view, distanceRecorder.getDistance(view));
+          }
+        });
+  }
+
+  /**
+   * Returns an iterable which iterates thru the provided view and its children in a
+   * depth-first, in-order traversal. That is to say that for a view such as:
+   *      Root
+   *     /  |  \
+   *     A  R  U
+   *    /|  |\
+   *   B D  G N
+   * Will be iterated: Root, A, B, D, R, G, N, U.
+   *
+   * @param root the non-null, root view.
+   */
+  public static Iterable<View> depthFirstViewTraversal(View root) {
+    return depthFirstTraversal(root, VIEW_TREE_VIEWER);
+  }
+
+  /**
+   * Returns an iterable which iterates thru the provided view and its children in a
+   * breadth-first, row-level-order traversal. That is to say that for a view such as:
+   *      Root
+   *     /  |  \
+   *     A  R  U
+   *    /|  |\
+   *   B D  G N
+   * Will be iterated: Root, A, R, U, B, D, G, N
+   *
+   * @param root the non-null, root view.
+   */
+  public static Iterable<View> breadthFirstViewTraversal(View root) {
+    return breadthFirstTraversal(root, VIEW_TREE_VIEWER);
+  }
+
+  /**
+   * Creates a depth first traversing iterator of the tree rooted at root.
+   *
+   * @param root the root of the tree
+   * @param viewer a TreeViewer which can determine leafiness of any instance of T and generate
+   *   Iterables for the direct children of any instance of T.
+   */
+  @VisibleForTesting
+  static <T> Iterable<T> depthFirstTraversal(final T root, final TreeViewer<T> viewer) {
+    checkNotNull(root);
+    checkNotNull(viewer);
+    return new TreeTraversalIterable<T>(root, TraversalStrategy.DEPTH_FIRST, viewer);
+  }
+
+  /**
+   * Creates a breadth first traversing iterator of the tree rooted at root.
+   *
+   * @param root the root of the tree
+   * @param viewer a TreeViewer which can determine leafiness of any instance of T and generate
+   *   Iterables for the direct children of any instance of T.
+   */
+  @VisibleForTesting
+  static <T> Iterable<T> breadthFirstTraversal(final T root, final TreeViewer<T> viewer) {
+    checkNotNull(root);
+    checkNotNull(viewer);
+    return new TreeTraversalIterable<T>(root, TraversalStrategy.BREADTH_FIRST, viewer);
+  }
+
+  /**
+   * Converts a tree into an Iterable of the tree's nodes presented in a given traversal order.
+   */
+  private static class TreeTraversalIterable<T> implements Iterable<T> {
+    private final T root;
+    private final TraversalStrategy traversalStrategy;
+    private final TreeViewer<T> treeViewer;
+
+    private TreeTraversalIterable(T root, TraversalStrategy traversalStrategy,
+        TreeViewer<T> treeViewer) {
+      this.root = checkNotNull(root);
+      this.traversalStrategy = checkNotNull(traversalStrategy);
+      this.treeViewer = checkNotNull(treeViewer);
+    }
+
+    @Override
+    public Iterator<T> iterator() {
+      final LinkedList<T> nodes = Lists.newLinkedList();
+      nodes.add(root);
+      return new AbstractIterator<T>() {
+        @Override
+        public T computeNext() {
+          if (nodes.isEmpty()) {
+            return endOfData();
+          } else {
+            T nextItem = checkNotNull(traversalStrategy.next(nodes), "Null items not allowed!");
+            traversalStrategy.combineNewChildren(nodes, treeViewer.children(nextItem));
+            return nextItem;
+          }
+        }
+      };
+    }
+  }
+
+  private enum TraversalStrategy {
+    BREADTH_FIRST() {
+      @Override
+      <T> void combineNewChildren(LinkedList<T> nodes, Collection<T> newChildren) {
+        nodes.addAll(newChildren);
+      }
+    }, DEPTH_FIRST() {
+      @Override
+      <T> void combineNewChildren(LinkedList<T> nodes, Collection<T> newChildren) {
+        nodes.addAll(0, newChildren);
+      }
+    };
+
+    abstract <T> void combineNewChildren(LinkedList<T> nodes, Collection<T> newChildren);
+    <T> T next(LinkedList<T> nodes) {
+      return nodes.removeFirst();
+    }
+
+  }
+
+  /**
+   * A TreeView providing access to the children of a given View.
+   *
+   * The only way views can have children is if they are a subclass of
+   * ViewGroup.
+   */
+  @VisibleForTesting
+  static class ViewTreeViewer implements TreeViewer<View> {
+    @Override
+    public Collection<View> children(View view) {
+      checkNotNull(view);
+      if (view instanceof ViewGroup) {
+        ViewGroup group = (ViewGroup) view;
+        int childCount = group.getChildCount();
+        List<View> children = Lists.newArrayList();
+        for (int i = 0; i < childCount; i++) {
+          children.add(group.getChildAt(i));
+        }
+        return children;
+      } else {
+        return Collections.<View>emptyList();
+      }
+    }
+  }
+
+  /**
+   * Provides a tree view of items of instance T and records their distance from
+   * a well known root.
+   *
+   * It is assumed that this TreeViewer will only be called with nodes that it
+   * has processed via its children method, or the root node itself. Otherwise it
+   * will not be able to determine distance from the root and an exception will be thrown.
+   *
+   * This class is stateful and only provides the correct distances after the underlying
+   * tree has been iterated over.
+   */
+  @VisibleForTesting
+  static class DistanceRecordingTreeViewer<T> implements TreeViewer<T> {
+    private final T root;
+    private final Map<T, Integer> nodeToDistance = Maps.newHashMap();
+    private final TreeViewer<T> delegateViewer;
+
+    DistanceRecordingTreeViewer(T root, TreeViewer<T> delegateViewer) {
+      this.root = checkNotNull(root);
+      this.delegateViewer = checkNotNull(delegateViewer);
+    }
+
+    int getDistance(T node) {
+      return checkNotNull(nodeToDistance.get(node), "Never seen %s before", node);
+    }
+
+    @Override
+    public Collection<T> children(final T node) {
+      if (node == root) {
+        // base case.
+        nodeToDistance.put(node, 0);
+      }
+
+      int myDistance = getDistance(node);
+      final int childDistance = myDistance + 1;
+      Collection<T> children = delegateViewer.children(node);
+
+      for (T child : children) {
+        nodeToDistance.put(child, childDistance);
+      }
+      return children;
+    }
+  }
+
+  /**
+   * Provides a way of viewing any instance of T as a tree so long as there exists a method
+   * for converting the instance of T into a Collection of that instance's direct children.
+   *
+   * This nice, sensible abstraction for dealing with objects with treelike properties was
+   * stolen from Guava's bug tracker. The Guava team is still working out the way trees
+   * should be exposed as Guava collections - so we have to provide our own.
+   */
+  @VisibleForTesting
+  interface TreeViewer<T> {
+
+    /**
+     * Returns a collection view of the children of this node.
+     */
+    Collection<T> children(T instance);
+  }
+
+
+
+  /**
+   * Represents the distance a given view is from the root view.
+   */
+  public static class ViewAndDistance {
+    private final View view;
+    private final int distanceFromRoot;
+
+    private ViewAndDistance(View view, int distanceFromRoot) {
+      this.view = view;
+      this.distanceFromRoot = distanceFromRoot;
+    }
+
+    public View getView() {
+      return view;
+    }
+
+    public int getDistanceFromRoot() {
+      return distanceFromRoot;
+    }
+  }
+}
diff --git a/espresso/espresso-sample/build.gradle b/espresso/espresso-sample/build.gradle
new file mode 100644
index 0000000..908bc25
--- /dev/null
+++ b/espresso/espresso-sample/build.gradle
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * 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.
+ */
+
+apply plugin: 'android'
+
+repositories {
+    maven { url '../../../../prebuilts/tools/common/m2/repository' }
+    maven { url '../../../../prebuilts/tools/common/m2/internal' }
+}
+
+android {
+    compileSdkVersion 19
+    buildToolsVersion "19.0.3"
+
+    packagingOptions {
+        exclude 'LICENSE.txt'
+    }
+
+    lintOptions {
+        abortOnError false
+    }
+
+    defaultConfig {
+        testInstrumentationRunner "com.google.android.apps.common.testing.testrunner.GoogleInstrumentationTestRunner"
+    }
+}
+
+dependencies {
+    compile files('../libs/guava-14.0.1.jar')
+    compile 'com.android.support:support-v4:19.1.+'
+    compile 'com.android.support:appcompat-v7:19.1.+'
+    compile project(':idling-resource-interface')
+    androidTestCompile project(':espresso-contrib')
+}
diff --git a/espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/ActionBarTest.java b/espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/ActionBarTest.java
new file mode 100644
index 0000000..10485c9
--- /dev/null
+++ b/espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/ActionBarTest.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.testapp;
+
+import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView;
+import static com.google.android.apps.common.testing.ui.espresso.Espresso.openActionBarOverflowOrOptionsMenu;
+import static com.google.android.apps.common.testing.ui.espresso.Espresso.openContextualActionModeOverflowMenu;
+import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click;
+import static com.google.android.apps.common.testing.ui.espresso.assertion.ViewAssertions.matches;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withText;
+
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+
+/**
+ * Demonstrates Espresso with action bar and contextual action mode. 
+ * {@link openActionBarOverflowOrOptionsMenu()} opens the overflow menu from an action bar.
+ * {@link openContextualActionModeOverflowMenu()} opens the overflow menu from an contextual action
+ * mode.
+ */
+@LargeTest
+public class ActionBarTest extends ActivityInstrumentationTestCase2<ActionBarTestActivity> {
+  @SuppressWarnings("deprecation")
+  public ActionBarTest() {
+    // This constructor was deprecated - but we want to support lower API levels.
+    super("com.google.android.apps.common.testing.ui.testapp", ActionBarTestActivity.class);
+  }
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    // Espresso will not launch our activity for us, we must launch it via getActivity().
+    getActivity();
+  }
+
+  @SuppressWarnings("unchecked")
+  public void testClickActionBarItem() {
+    onView(withId(R.id.hide_contextual_action_bar))
+      .perform(click());
+
+    onView(withId(R.id.action_save))
+      .perform(click());
+
+    onView(withId(R.id.text_action_bar_result))
+      .check(matches(withText("Save")));
+  }
+
+  @SuppressWarnings("unchecked")
+  public void testClickActionModeItem() {
+    onView(withId(R.id.show_contextual_action_bar))
+      .perform(click());
+
+    onView((withId(R.id.action_lock)))
+      .perform(click());
+
+    onView(withId(R.id.text_action_bar_result))
+      .check(matches(withText("Lock")));
+  }
+
+
+  @SuppressWarnings("unchecked")
+  public void testActionBarOverflow() {
+    onView(withId(R.id.hide_contextual_action_bar))
+      .perform(click());
+
+    // Open the overflow menu from action bar
+    openActionBarOverflowOrOptionsMenu(getInstrumentation().getTargetContext());
+
+    onView(withText("World"))
+      .perform(click());
+
+    onView(withId(R.id.text_action_bar_result))
+      .check(matches(withText("World")));
+  }
+
+  @SuppressWarnings("unchecked")
+  public void testActionModeOverflow() {
+    onView(withId(R.id.show_contextual_action_bar))
+      .perform(click());
+
+    // Open the overflow menu from contextual action mode.
+    openContextualActionModeOverflowMenu();
+
+    onView(withText("Key"))
+      .perform(click());
+
+    onView(withId(R.id.text_action_bar_result))
+      .check(matches(withText("Key")));
+  }
+}
diff --git a/espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/AdapterViewTest.java b/espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/AdapterViewTest.java
new file mode 100644
index 0000000..fd531ec
--- /dev/null
+++ b/espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/AdapterViewTest.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.testapp;
+
+import static com.google.android.apps.common.testing.ui.espresso.Espresso.onData;
+import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView;
+import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click;
+import static com.google.android.apps.common.testing.ui.espresso.assertion.ViewAssertions.matches;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withText;
+import static com.google.android.apps.common.testing.ui.testapp.LongListMatchers.isFooter;
+import static com.google.android.apps.common.testing.ui.testapp.LongListMatchers.withItemContent;
+import static com.google.android.apps.common.testing.ui.testapp.LongListMatchers.withItemSize;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.view.View;
+import android.widget.Adapter;
+import android.widget.AdapterView;
+
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+
+/**
+ * Demonstrates the usage of
+ * {@link com.google.android.apps.common.testing.ui.espresso.Espresso#onData(org.hamcrest.Matcher)}
+ * to match data within list views.
+ */
+@LargeTest
+public class AdapterViewTest extends ActivityInstrumentationTestCase2<LongListActivity> {
+
+  @SuppressWarnings("deprecation")
+  public AdapterViewTest() {
+    // This constructor was deprecated - but we want to support lower API levels.
+    super("com.google.android.apps.common.testing.ui.testapp", LongListActivity.class);
+  }
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    getActivity();
+  }
+
+  public void testClickOnItem50() {
+    // The text view "item: 50" may not exist if we haven't scrolled to it.
+    // By using onData api we tell Espresso to look into the Adapter for an item matching
+    // the matcher we provide it. Espresso will then bring that item into the view hierarchy
+    // and we can click on it.
+
+    onData(withItemContent("item: 50"))
+      .perform(click());
+
+    onView(withId(R.id.selection_row_value))
+      .check(matches(withText("50")));
+  }
+
+  public void testClickOnSpecificChildOfRow60() {
+    onData(withItemContent("item: 60"))
+      .onChildView(withId(R.id.item_size)) // resource id of second column from xml layout
+      .perform(click());
+
+    onView(withId(R.id.selection_row_value))
+      .check(matches(withText("60")));
+
+    onView(withId(R.id.selection_column_value))
+      .check(matches(withText("2")));
+  }
+
+  public void testClickOnFirstAndFifthItemOfLength8() {
+    onData(is(withItemSize(8)))
+      .atPosition(0)
+      .perform(click());
+
+    onView(withId(R.id.selection_row_value))
+      .check(matches(withText("10")));
+
+    onData(is(withItemSize(8)))
+      .atPosition(4)
+      .perform(click());
+
+    onView(withId(R.id.selection_row_value))
+      .check(matches(withText("14")));
+  }
+
+  @SuppressWarnings("unchecked")
+  public void testClickFooter() {
+    onData(isFooter())
+      .perform(click());
+
+    onView(withId(R.id.selection_row_value))
+      .check(matches(withText("100")));
+  }
+
+  @SuppressWarnings("unchecked")
+  public void testDataItemNotInAdapter(){
+    onView(withId(R.id.list))
+      .check(matches(not(withAdaptedData(withItemContent("item: 168")))));
+  }
+
+  private static Matcher<View> withAdaptedData(final Matcher<Object> dataMatcher) {
+    return new TypeSafeMatcher<View>() {
+
+      @Override
+      public void describeTo(Description description) {
+        description.appendText("with class name: ");
+        dataMatcher.describeTo(description);
+      }
+
+      @Override
+      public boolean matchesSafely(View view) {
+        if (!(view instanceof AdapterView)) {
+          return false;
+        }
+        @SuppressWarnings("rawtypes")
+        Adapter adapter = ((AdapterView) view).getAdapter();
+        for (int i = 0; i < adapter.getCount(); i++) {
+          if (dataMatcher.matches(adapter.getItem(i))) {
+            return true;
+          }
+        }
+        return false;
+      }
+    };
+  }
+}
diff --git a/espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/AdvancedSynchronizationTest.java b/espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/AdvancedSynchronizationTest.java
new file mode 100644
index 0000000..d19fb69
--- /dev/null
+++ b/espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/AdvancedSynchronizationTest.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.testapp;
+
+import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView;
+import static com.google.android.apps.common.testing.ui.espresso.Espresso.registerIdlingResources;
+import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click;
+import static com.google.android.apps.common.testing.ui.espresso.assertion.ViewAssertions.matches;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withText;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.android.apps.common.testing.ui.espresso.contrib.CountingIdlingResource;
+import com.google.android.apps.common.testing.ui.testapp.SyncActivity.HelloWorldServer;
+
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+
+/**
+ * Example for {@link CountingIdlingResource}. Demonstrates how to wait on a delayed response from
+ * request before continuing with a test.
+ */
+@LargeTest
+public class AdvancedSynchronizationTest extends ActivityInstrumentationTestCase2<SyncActivity> {
+
+  private class DecoratedHelloWorldServer implements HelloWorldServer {
+    private final HelloWorldServer realHelloWorldServer;
+    private final CountingIdlingResource helloWorldServerIdlingResource;
+
+    private DecoratedHelloWorldServer(HelloWorldServer realHelloWorldServer,
+        CountingIdlingResource helloWorldServerIdlingResource) {
+      this.realHelloWorldServer = checkNotNull(realHelloWorldServer);
+      this.helloWorldServerIdlingResource = checkNotNull(helloWorldServerIdlingResource);
+    }
+
+    @Override
+    public String getHelloWorld() {
+      // Use CountingIdlingResource to track in-flight calls to getHelloWorld (a simulation of a
+      // network call). Whenever the count goes to zero, Espresso will be notified that this
+      // resource is idle and the test will be able to proceed.
+      helloWorldServerIdlingResource.increment();
+      try {
+        return realHelloWorldServer.getHelloWorld();
+      } finally {
+        helloWorldServerIdlingResource.decrement();
+      }
+    }
+  }
+
+  @SuppressWarnings("deprecation")
+  public AdvancedSynchronizationTest() {
+    // This constructor was deprecated - but we want to support lower API levels.
+    super("com.google.android.apps.common.testing.ui.testapp", SyncActivity.class);
+  }
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    SyncActivity activity = getActivity();
+    HelloWorldServer realServer = activity.getHelloWorldServer();
+    // Here, we use CountingIdlingResource - a common convenience class - to track the idle state of
+    // the server. You could also do this yourself, by implementing the IdlingResource interface.
+    CountingIdlingResource countingResource = new CountingIdlingResource("HelloWorldServerCalls");
+    activity.setHelloWorldServer(new DecoratedHelloWorldServer(realServer, countingResource));
+    registerIdlingResources(countingResource);
+  }
+
+  public void testCountingIdlingResource() {
+    // Request the "hello world!" text by clicking on the request button.
+    onView(withId(R.id.request_button)).perform(click());
+
+    // Espresso waits for the resource to go idle and then continues.
+
+    // The check if the text is visible can pass now.
+    onView(withId(R.id.status_text)).check(matches(withText(R.string.hello_world)));
+  }
+}
diff --git a/espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/BasicTest.java b/espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/BasicTest.java
new file mode 100644
index 0000000..ba2b282
--- /dev/null
+++ b/espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/BasicTest.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.testapp;
+
+import static com.google.android.apps.common.testing.ui.espresso.Espresso.onData;
+import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView;
+import static com.google.android.apps.common.testing.ui.espresso.Espresso.pressBack;
+import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click;
+import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.typeText;
+import static com.google.android.apps.common.testing.ui.espresso.assertion.ViewAssertions.matches;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withText;
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.is;
+
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+
+/**
+ * Highlights basic
+ * {@link com.google.android.apps.common.testing.ui.espresso.Espresso#onView(org.hamcrest.Matcher)}
+ * functionality.
+ */
+@LargeTest
+public class BasicTest extends ActivityInstrumentationTestCase2<SimpleActivity> {
+
+  @SuppressWarnings("deprecation")
+  public BasicTest() {
+    // This constructor was deprecated - but we want to support lower API levels.
+    super("com.google.android.apps.common.testing.ui.testapp", SimpleActivity.class);
+  }
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    // Espresso will not launch our activity for us, we must launch it via getActivity().
+    getActivity();
+  }
+
+  public void testSimpleClickAndCheckText() {
+    onView(withId(R.id.button_simple))
+        .perform(click());
+
+    onView(withId(R.id.text_simple))
+        .check(matches(withText("Hello Espresso!")));
+  }
+
+  public void testTypingAndPressBack() {
+    onView(withId(R.id.sendtext_simple))
+        .perform(typeText("Have a cup of Espresso."));
+
+    onView(withId(R.id.send_simple))
+        .perform(click());
+
+    // Clicking launches a new activity that shows the text entered above. You don't need to do
+    // anything special to handle the activity transitions. Espresso takes care of waiting for the
+    // new activity to be resumed and its view hierarchy to be laid out.
+    onView(withId(R.id.display_data))
+        .check(matches(withText(("Have a cup of Espresso."))));
+
+    // Going back to the previous activity - lets make sure our text was perserved.
+    pressBack();
+
+    onView(withId(R.id.sendtext_simple))
+        .check(matches(withText(containsString("Espresso"))));
+  }
+
+  @SuppressWarnings("unchecked")
+  public void testClickOnSpinnerItemAmericano(){
+    // Open the spinner.
+    onView(withId(R.id.spinner_simple))
+      .perform(click());
+    // Spinner creates a List View with its contents - this can be very long and the element not
+    // contributed to the ViewHierarchy - by using onData we force our desired element into the
+    // view hierarchy.
+    onData(allOf(is(instanceOf(String.class)), is("Americano")))
+      .perform(click());
+
+    onView(withId(R.id.spinnertext_simple))
+      .check(matches(withText(containsString("Americano"))));
+  }
+}
+
+
diff --git a/espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/CustomFailureHandlerTest.java b/espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/CustomFailureHandlerTest.java
new file mode 100644
index 0000000..14a3baf
--- /dev/null
+++ b/espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/CustomFailureHandlerTest.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.testapp;
+
+import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView;
+import static com.google.android.apps.common.testing.ui.espresso.Espresso.setFailureHandler;
+import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withText;
+
+import com.google.android.apps.common.testing.ui.espresso.FailureHandler;
+import com.google.android.apps.common.testing.ui.espresso.NoMatchingViewException;
+import com.google.android.apps.common.testing.ui.espresso.base.DefaultFailureHandler;
+
+import android.content.Context;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+import android.view.View;
+
+import org.hamcrest.Matcher;
+
+/**
+ * A sample of how to set a non-default {@link FailureHandler}.
+ */
+@LargeTest
+public class CustomFailureHandlerTest extends ActivityInstrumentationTestCase2<MainActivity> {
+
+  private static final String TAG = "CustomFailureHandlerTest";
+
+  @SuppressWarnings("deprecation")
+  public CustomFailureHandlerTest() {
+    // This constructor was deprecated - but we want to support lower API levels.
+    super("com.google.android.apps.common.testing.ui.testapp", MainActivity.class);
+  }
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    getActivity();
+    setFailureHandler(new CustomFailureHandler(getInstrumentation().getTargetContext()));
+  }
+
+  public void testWithCustomFailureHandler() {
+    try {
+      onView(withText("does not exist")).perform(click());
+    } catch (MySpecialException expected) {
+      Log.e(TAG, "Special exception is special and expected: ", expected);
+    }
+  }
+
+  /**
+   * A {@link FailureHandler} that re-throws {@link NoMatchingViewException} as
+   * {@link MySpecialException}. All other functionality is delegated to
+   * {@link DefaultFailureHandler}.
+   */
+  private static class CustomFailureHandler implements FailureHandler {
+    private final FailureHandler delegate;
+
+    public CustomFailureHandler(Context targetContext) {
+      delegate = new DefaultFailureHandler(targetContext);
+    }
+
+    @Override
+    public void handle(Throwable error, Matcher<View> viewMatcher) {
+      try {
+        delegate.handle(error, viewMatcher);
+      } catch (NoMatchingViewException e) {
+        throw new MySpecialException(e);
+      }
+    }
+  }
+
+  private static class MySpecialException extends RuntimeException {
+    MySpecialException(Throwable cause) {
+      super(cause);
+    }
+  }
+}
diff --git a/espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/DrawerActionsTest.java b/espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/DrawerActionsTest.java
new file mode 100644
index 0000000..b7c1337
--- /dev/null
+++ b/espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/DrawerActionsTest.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.testapp;
+
+import static com.google.android.apps.common.testing.ui.espresso.Espresso.onData;
+import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView;
+import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click;
+import static com.google.android.apps.common.testing.ui.espresso.assertion.ViewAssertions.matches;
+import static com.google.android.apps.common.testing.ui.espresso.contrib.DrawerActions.closeDrawer;
+import static com.google.android.apps.common.testing.ui.espresso.contrib.DrawerActions.openDrawer;
+import static com.google.android.apps.common.testing.ui.espresso.contrib.DrawerMatchers.isClosed;
+import static com.google.android.apps.common.testing.ui.espresso.contrib.DrawerMatchers.isOpen;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withText;
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.is;
+
+import com.google.android.apps.common.testing.ui.espresso.contrib.DrawerActions;
+
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+
+/**
+ * Demonstrates use of {@link DrawerActions}.
+ */
+@LargeTest
+public class DrawerActionsTest  extends ActivityInstrumentationTestCase2<DrawerActivity> {
+
+  public DrawerActionsTest() {
+    super(DrawerActivity.class);
+  }
+
+  @Override
+  protected void setUp() throws Exception {
+    super.setUp();
+    getActivity();
+  }
+
+  public void testOpenAndCloseDrawer() {
+    // Drawer should not be open to start.
+    onView(withId(R.id.drawer_layout)).check(matches(isClosed()));
+
+    openDrawer(R.id.drawer_layout);
+
+    // The drawer should now be open.
+    onView(withId(R.id.drawer_layout)).check(matches(isOpen()));
+
+    closeDrawer(R.id.drawer_layout);
+
+    // Drawer should be closed again.
+    onView(withId(R.id.drawer_layout)).check(matches(isClosed()));
+  }
+
+  @SuppressWarnings("unchecked")
+  public void testDrawerOpenAndClick() {
+    openDrawer(R.id.drawer_layout);
+
+    onView(withId(R.id.drawer_layout)).check(matches(isOpen()));
+
+    // Click an item in the drawer. We use onData because the drawer is backed by a ListView, and
+    // the item may not necessarily be visible in the view hierarchy.
+    int rowIndex = 2;
+    String rowContents = DrawerActivity.DRAWER_CONTENTS[rowIndex];
+    onData(allOf(is(instanceOf(String.class)), is(rowContents))).perform(click());
+
+    // clicking the item should close the drawer.
+    onView(withId(R.id.drawer_layout)).check(matches(isClosed()));
+
+    // The text view will now display "You picked: Pickle"
+    onView(withId(R.id.drawer_text_view)).check(matches(withText("You picked: " + rowContents)));
+  }
+}
diff --git a/espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/LongListMatchers.java b/espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/LongListMatchers.java
new file mode 100644
index 0000000..1518697
--- /dev/null
+++ b/espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/LongListMatchers.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.testapp;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasEntry;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.is;
+
+import com.google.android.apps.common.testing.ui.espresso.matcher.BoundedMatcher;
+
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+
+import java.util.Map;
+
+/**
+ * Static utility methods to create {@link Matcher} instances that can be applied to the data
+ * objects created by {@link com.google.android.apps.common.testing.ui.testapp.LongListActivity}.
+ * <p>
+ * These matchers are used by the
+ * {@link com.google.android.apps.common.testing.ui.espresso.Espresso#onData(Matcher)} API and are
+ * applied against the data exposed by @{link android.widget.ListView#getAdapter()}.
+ * </p>
+ * <p>
+ * In LongListActivity's case - each row is a Map containing 2 key value pairs. The key "STR" is
+ * mapped to a String which will be rendered into a TextView with the R.id.item_content. The other
+ * key "LEN" is an Integer which is the length of the string "STR" refers to. This length is
+ * rendered into a TextView with the id R.id.item_size.
+ * </p>
+ */
+public final class LongListMatchers {
+
+  private LongListMatchers() { }
+
+
+  /**
+   * Creates a matcher against the text stored in R.id.item_content. This text is roughly
+   * "item: $row_number".
+   */
+  public static Matcher<Object> withItemContent(String expectedText) {
+    // use preconditions to fail fast when a test is creating an invalid matcher.
+    checkNotNull(expectedText);
+    return withItemContent(equalTo(expectedText));
+  }
+
+  /**
+   * Creates a matcher against the text stored in R.id.item_content. This text is roughly
+   * "item: $row_number".
+   */
+  @SuppressWarnings("rawtypes")
+  public static Matcher<Object> withItemContent(final Matcher<String> itemTextMatcher) {
+    // use preconditions to fail fast when a test is creating an invalid matcher.
+    checkNotNull(itemTextMatcher);
+    return new BoundedMatcher<Object, Map>(Map.class) {
+      @Override
+      public boolean matchesSafely(Map map) {
+        return hasEntry(equalTo("STR"), itemTextMatcher).matches(map);
+      }
+
+      @Override
+      public void describeTo(Description description) {
+        description.appendText("with item content: ");
+        itemTextMatcher.describeTo(description);
+      }
+    };
+  }
+
+  /**
+   * Creates a matcher against the text stored in R.id.item_size. This text is the size of the text
+   * printed in R.id.item_content.
+   */
+  public static Matcher<Object> withItemSize(int itemSize) {
+    // use preconditions to fail fast when a test is creating an invalid matcher.
+    checkArgument(itemSize > -1);
+    return withItemSize(equalTo(itemSize));
+  }
+
+  /**
+   * Creates a matcher against the text stored in R.id.item_size. This text is the size of the text
+   * printed in R.id.item_content.
+   */
+  @SuppressWarnings("rawtypes")
+  public static Matcher<Object> withItemSize(final Matcher<Integer> itemSizeMatcher) {
+    // use preconditions to fail fast when a test is creating an invalid matcher.
+    checkNotNull(itemSizeMatcher);
+    return new BoundedMatcher<Object, Map>(Map.class) {
+      @Override
+      public boolean matchesSafely(Map map) {
+        return hasEntry(equalTo("LEN"), itemSizeMatcher).matches(map);
+      }
+
+      @Override
+      public void describeTo(Description description) {
+        description.appendText("with item size: ");
+        itemSizeMatcher.describeTo(description);
+      }
+    };
+  }
+
+  /**
+   * Creates a matcher against the footer of this list view.
+   */
+  @SuppressWarnings("unchecked")
+  public static Matcher<Object> isFooter() {
+    // This depends on LongListActivity.FOOTER being passed as data in the addFooterView method.
+    return allOf(is(instanceOf(String.class)), is(LongListActivity.FOOTER));
+  }
+
+}
diff --git a/espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/LongListMatchersTest.java b/espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/LongListMatchersTest.java
new file mode 100644
index 0000000..3b80757
--- /dev/null
+++ b/espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/LongListMatchersTest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.testapp;
+
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.assertThat;
+import static com.google.android.apps.common.testing.ui.testapp.LongListMatchers.withItemContent;
+import static com.google.android.apps.common.testing.ui.testapp.LongListMatchers.withItemSize;
+import static org.hamcrest.Matchers.anyOf;
+import static org.hamcrest.Matchers.endsWith;
+import static org.hamcrest.Matchers.equalTo;
+
+import android.content.Intent;
+import android.test.ActivityUnitTestCase;
+
+/**
+ * UnitTests for LongListMatchers matcher factory.
+ */
+public final class LongListMatchersTest extends ActivityUnitTestCase<LongListActivity> {
+
+  public LongListMatchersTest() {
+    super(LongListActivity.class);
+  }
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    startActivity(new Intent(getInstrumentation().getTargetContext(), LongListActivity.class),
+        null, null);
+  }
+
+  public void testWithContent() {
+    assertThat(getActivity().makeItem(54), withItemContent("item: 54"));
+    assertThat(getActivity().makeItem(54), withItemContent(endsWith("54")));
+    assertFalse(withItemContent("hello world").matches(getActivity().makeItem(54)));
+  }
+
+  @SuppressWarnings("unchecked")
+  public void testWithItemSize() {
+    assertThat(getActivity().makeItem(54), withItemSize(8));
+    assertThat(getActivity().makeItem(54), withItemSize(anyOf(equalTo(8), equalTo(7))));
+    assertFalse(withItemSize(7).matches(getActivity().makeItem(54)));
+  }
+}
diff --git a/espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/MenuTest.java b/espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/MenuTest.java
new file mode 100644
index 0000000..9ea8898
--- /dev/null
+++ b/espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/MenuTest.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.testapp;
+
+import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView;
+import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click;
+import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.longClick;
+import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.pressMenuKey;
+import static com.google.android.apps.common.testing.ui.espresso.assertion.ViewAssertions.doesNotExist;
+import static com.google.android.apps.common.testing.ui.espresso.assertion.ViewAssertions.matches;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isDisplayed;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isRoot;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withText;
+
+import android.os.Build;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+
+/**
+ * Ensures view root ordering works properly.
+ */
+@LargeTest
+public class MenuTest extends ActivityInstrumentationTestCase2<MenuActivity> {
+  @SuppressWarnings("deprecation")
+  public MenuTest() {
+    // This constructor was deprecated - but we want to support lower API levels.
+    super("com.google.android.apps.common.testing.ui.testapp", MenuActivity.class);
+  }
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    getActivity();
+  }
+
+  public void testPopupMenu() {
+    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
+      // popup menus are post honeycomb.
+      return;
+    }
+    onView(withText(R.string.popup_item_1_text)).check(doesNotExist());
+    onView(withId(R.id.popup_button)).perform(click());
+    onView(withText(R.string.popup_item_1_text)).check(matches(isDisplayed())).perform(click());
+
+    onView(withId(R.id.text_menu_result)).check(matches(withText(R.string.popup_item_1_text)));
+  }
+
+  public void testContextMenu() {
+    onView(withText(R.string.context_item_2_text)).check(doesNotExist());
+    onView(withId(R.id.text_context_menu)).perform(longClick());
+    onView(withText(R.string.context_item_2_text)).check(matches(isDisplayed())).perform(click());
+
+    onView(withId(R.id.text_menu_result)).check(matches(withText(R.string.context_item_2_text)));
+  }
+
+  public void testOptionMenu() {
+    onView(withText(R.string.options_item_3_text)).check(doesNotExist());
+    onView(isRoot()).perform(pressMenuKey());
+    onView(withText(R.string.options_item_3_text)).check(matches(isDisplayed())).perform(click());
+
+    onView(withId(R.id.text_menu_result)).check(matches(withText(R.string.options_item_3_text)));
+  }
+}
diff --git a/espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/MultipleWindowTest.java b/espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/MultipleWindowTest.java
new file mode 100644
index 0000000..23c3ea3
--- /dev/null
+++ b/espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/MultipleWindowTest.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.testapp;
+
+import static com.google.android.apps.common.testing.ui.espresso.Espresso.onData;
+import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView;
+import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.clearText;
+import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click;
+import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.scrollTo;
+import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.typeText;
+import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.typeTextIntoFocusedView;
+import static com.google.android.apps.common.testing.ui.espresso.assertion.ViewAssertions.matches;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.RootMatchers.withDecorView;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withText;
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+
+import android.os.Build;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+
+/**
+ * Demonstrates dealing with multiple windows.
+ *
+ * Espresso provides the ability to switch the default window matcher used in both onView and onData
+ * interactions.
+ *
+ * @see com.google.android.apps.common.testing.ui.espresso.Espresso#onView
+ * @see com.google.android.apps.common.testing.ui.espresso.Espresso#onData
+ */
+@LargeTest
+public class MultipleWindowTest extends ActivityInstrumentationTestCase2<SendActivity> {
+
+  @SuppressWarnings("deprecation")
+  public MultipleWindowTest() {
+    // This constructor was deprecated - but we want to support lower API levels.
+    super("com.google.android.apps.common.testing.ui.testapp", SendActivity.class);
+  }
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    // Espresso will not launch our activity for us, we must launch it via getActivity().
+    getActivity();
+  }
+
+  public void testInteractionsWithAutoCompletePopup() {
+    if (Build.VERSION.SDK_INT < 10) {
+      // Froyo's AutoCompleteTextBox is broken - do not bother testing with it.
+      return;
+    }
+    // Android's Window system allows multiple view hierarchies to layer on top of each other.
+    //
+    // A real world analogy would be an overhead projector with multiple transparencies placed
+    // on top of each other. Each Window is a transparency, and what is drawn on top of this
+    // transparency is the view hierarchy.
+    //
+    // By default Espresso uses a heuristic to guess which Window you intend to interact with.
+    // This heuristic is normally 'good enough' however if you want to interact with a Window
+    // that it does not select then you'll have to swap in your own root window matcher.
+
+
+    // Initially we only have 1 window, but by typing into the auto complete text view another
+    // window will be layered on top of the screen. Espresso ignore's this layer because it is
+    // not connected to the keyboard/ime.
+    onView(withId(R.id.auto_complete_text_view))
+        .perform(scrollTo())
+        .perform(typeText("So"));
+
+    // As you can see, we continue typing oblivious to the new window on the screen.
+    // At the moment there should be 2 completions (South China Sea and Southern Ocean)
+    // Lets narrow that down to 1 completion.
+    onView(withId(R.id.auto_complete_text_view))
+        .perform(typeTextIntoFocusedView("uth "));
+
+    // Now we may want to explicitly tap on a completion. We must override Espresso's
+    // default window selection heuristic with our own.
+    onView(withText("South China Sea"))
+        .inRoot(withDecorView(not(is(getActivity().getWindow().getDecorView()))))
+        .perform(click());
+
+    // And by clicking on the auto complete term, the text should be filled in.
+    onView(withId(R.id.auto_complete_text_view))
+        .check(matches(withText("South China Sea")));
+
+
+    // NB: The autocompletion box is implemented with a ListView, so the preferred way
+    // to interact with it is onData(). We can use inRoot here too!
+    onView(withId(R.id.auto_complete_text_view))
+        .perform(clearText())
+        .perform(typeText("S"));
+
+    // Which is useful because some of the completions may not be part of the View Hierarchy
+    // unless you scroll around the list.
+    onData(allOf(instanceOf(String.class), is("Baltic Sea")))
+        .inRoot(withDecorView(not(is(getActivity().getWindow().getDecorView()))))
+        .perform(click());
+
+    onView(withId(R.id.auto_complete_text_view))
+        .check(matches(withText("Baltic Sea")));
+  }
+
+}
+
+
diff --git a/espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/ScrollToTest.java b/espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/ScrollToTest.java
new file mode 100644
index 0000000..6cf7836
--- /dev/null
+++ b/espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/ScrollToTest.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.testapp;
+
+import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView;
+import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click;
+import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.scrollTo;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId;
+import static org.hamcrest.Matchers.is;
+
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+
+
+/**
+ * Demonstrates the usage of
+ * {@link com.google.android.apps.common.testing.ui.espresso.action.ViewActions#scrollTo()}.
+ */
+@LargeTest
+public class ScrollToTest extends ActivityInstrumentationTestCase2<ScrollActivity> {
+
+  @SuppressWarnings("deprecation")
+  public ScrollToTest() {
+    // This constructor was deprecated - but we want to support lower API levels.
+    super("com.google.android.apps.common.testing.ui.testapp", ScrollActivity.class);
+  }
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    // Espresso will not launch our activity for us, we must launch it via getActivity().
+    getActivity();
+  }
+
+  // You can pass more than one action to perform. This is useful if you are performing two actions
+  // back-to-back on the same view.
+  // Note - scrollTo is a no-op if the view is already displayed on the screen.
+  public void testScrollToInScrollView() {
+    onView(withId(is(R.id.bottom_left)))
+      .perform(scrollTo(), click());
+  }
+}
diff --git a/espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/SwipeTest.java b/espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/SwipeTest.java
new file mode 100644
index 0000000..46704ec
--- /dev/null
+++ b/espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/SwipeTest.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.testapp;
+
+import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView;
+import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.swipeLeft;
+import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.swipeRight;
+import static com.google.android.apps.common.testing.ui.espresso.assertion.ViewAssertions.matches;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isDisplayed;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId;
+import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withText;
+
+import com.google.android.apps.common.testing.ui.espresso.action.ViewActions;
+
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+
+/**
+ * Demonstrates use of {@link ViewActions#swipeLeft()} and {@link ViewActions#swipeRight()}.
+ */
+@LargeTest
+public class SwipeTest extends ActivityInstrumentationTestCase2<ViewPagerActivity> {
+
+  @SuppressWarnings("deprecation")
+  public SwipeTest() {
+    // This constructor was deprecated - but we want to support lower API levels.
+    super("com.google.android.apps.common.testing.ui.testapp", ViewPagerActivity.class);
+  }
+
+  @Override
+  protected void setUp() throws Exception {
+    super.setUp();
+    getActivity();
+  }
+
+  public void testSwipingThroughViews() {
+    // Should be on position 0 to start with.
+    onView(withText("Position #0")).check(matches(isDisplayed()));
+
+    // Swipe left once.
+    onView(withId(R.id.pager_layout)).perform(swipeLeft());
+
+    // Now position 1 should be visible.
+    onView(withText("Position #1")).check(matches(isDisplayed()));
+
+    // Swipe left again.
+    onView(withId(R.id.pager_layout)).perform(swipeLeft());
+
+    // Now position 2 should be visible.
+    onView(withText("Position #2")).check(matches(isDisplayed()));
+
+    // Swipe left again.
+    onView(withId(R.id.pager_layout)).perform(swipeLeft());
+
+    // Position 2 should still be visible as this is the last view in the pager.
+    onView(withText("Position #2")).check(matches(isDisplayed()));
+  }
+
+  public void testSwipingBackAndForward() {
+    // Should be on position 0 to start with.
+    onView(withText("Position #0")).check(matches(isDisplayed()));
+    
+    // Swipe left once.
+    onView(withId(R.id.pager_layout)).perform(swipeLeft());
+    
+    // Now position 1 should be visible.
+    onView(withText("Position #1")).check(matches(isDisplayed()));
+    
+    // Swipe back to the right.
+    onView(withId(R.id.pager_layout)).perform(swipeRight());
+    
+    // Now position 0 should be visible again.
+    onView(withText("Position #0")).check(matches(isDisplayed()));
+
+    // Swipe right again.
+    onView(withId(R.id.pager_layout)).perform(swipeRight());
+    
+    // Position 0 should still be visible as this is the first view in the pager.
+    onView(withText("Position #0")).check(matches(isDisplayed()));
+  }
+
+}
diff --git a/espresso/espresso-sample/src/main/AndroidManifest.xml b/espresso/espresso-sample/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..cf8e548
--- /dev/null
+++ b/espresso/espresso-sample/src/main/AndroidManifest.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2014 The Android Open Source Project
+  ~
+  ~ 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.
+  -->
+
+<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="ActionBarTestActivity" android:label="actionbar test activity" android:theme="@style/Theme.AppCompat.Light.DarkActionBar"/>
+        <activity android:name="SimpleActivity" android:label="simple activity"/>
+        <activity android:name="SendActivity" android:label="send activity"/>
+        <activity android:name="DisplayActivity" android:label="display activity"/>
+        <activity android:name="DrawerActivity" android:label="drawer activity" android:theme="@style/Theme.AppCompat.Light"/>
+        <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"/>
+        <activity android:name="FragmentStack" android:label="fragment stack activity"/>
+        <activity android:name="SyncActivity" android:label="sync activity"/>
+        <activity android:name="SimpleWebViewActivity" android:label="web view"/>
+        <activity android:name="SwipeActivity" android:label="swipe activity"/>
+        <activity android:name="ViewPagerActivity" android:label="view pager activity"/>
+    </application>
+
+    <uses-permission android:name="android.permission.CALL_PHONE"></uses-permission>
+</manifest>
diff --git a/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/ActionBarTestActivity.java b/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/ActionBarTestActivity.java
new file mode 100644
index 0000000..5c1ac60
--- /dev/null
+++ b/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/ActionBarTestActivity.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.testapp;
+
+import android.os.Bundle;
+import android.support.v7.app.ActionBarActivity;
+import android.support.v7.view.ActionMode;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+
+/**
+ * Shows ActionBar with a lot of items to get Action overflow on large displays. Click on item
+ * changes text of R.id.textActionBarResult.
+ */
+public class ActionBarTestActivity extends ActionBarActivity {
+  private ActionMode mode;
+  private MenuInflater inflater;
+
+  @Override
+  protected void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    setContentView(R.layout.actionbar_activity);
+    inflater = getMenuInflater();
+    mode = startSupportActionMode(new TestActionMode());
+
+    ((Button) findViewById(R.id.show_contextual_action_bar)).setOnClickListener(
+        new View.OnClickListener() {
+          @Override
+          public void onClick(View v) {
+            mode = startSupportActionMode(new TestActionMode());
+          }
+        });
+    ((Button) findViewById(R.id.hide_contextual_action_bar)).setOnClickListener(
+        new View.OnClickListener() {
+          @Override
+          public void onClick(View v) {
+            if (mode != null) {
+              mode.finish();
+            }
+          }
+        });
+  }
+
+  @Override
+  public boolean onCreateOptionsMenu(Menu menu) {
+    inflater.inflate(R.menu.actionbar_context_actions, menu);
+    return super.onCreateOptionsMenu(menu);
+  }
+
+  @Override
+  public boolean onOptionsItemSelected(MenuItem menu) {
+    setResult(menu.getTitle());
+    return true;
+  }
+
+  private void setResult(CharSequence result) {
+    TextView text = (TextView) findViewById(R.id.text_action_bar_result);
+    text.setText(result);
+  }
+
+  private final class TestActionMode implements ActionMode.Callback {
+    @Override
+    public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+      inflater.inflate(R.menu.actionbar_activity_actions, 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/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/DelegatingEditText.java b/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/DelegatingEditText.java
new file mode 100644
index 0000000..dace49c
--- /dev/null
+++ b/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/DelegatingEditText.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.testapp;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import android.widget.TextView.OnEditorActionListener;
+
+/**
+ * Custom edit text widget.
+ */
+public class DelegatingEditText extends LinearLayout {
+
+  private final EditText delegateEditText;
+  private final TextView messageView;
+  private final Context mContext;
+
+  public DelegatingEditText(Context context) {
+    this(context, null);
+  }
+
+  public DelegatingEditText(Context context, AttributeSet attrs) {
+    super(context, attrs);
+    setOrientation(VERTICAL);
+    mContext = context;
+    LayoutInflater inflater = LayoutInflater.from(context);
+    inflater.inflate(R.layout.delegating_edit_text, this, /* attachToRoot */ true);
+    messageView = (TextView) findViewById(R.id.edit_text_message);
+    delegateEditText = (EditText) findViewById(R.id.delegate_edit_text);
+    delegateEditText.setOnEditorActionListener(new OnEditorActionListener() {
+      @Override
+      public boolean onEditorAction(TextView v, int actionCode, KeyEvent event) {
+        messageView.setText("typed: " + delegateEditText.getText());
+        messageView.setVisibility(View.VISIBLE);
+        InputMethodManager imm =
+            (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
+        imm.hideSoftInputFromWindow(delegateEditText.getWindowToken(), 0);
+        return true;
+      }
+    });
+  }
+}
diff --git a/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/DisplayActivity.java b/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/DisplayActivity.java
new file mode 100644
index 0000000..d05ee00
--- /dev/null
+++ b/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/DisplayActivity.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.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.display_data);
+    textView.setText(getIntent().getStringExtra(SendActivity.EXTRA_DATA));
+  }
+}
diff --git a/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/DrawerActivity.java b/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/DrawerActivity.java
new file mode 100644
index 0000000..a13f688
--- /dev/null
+++ b/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/DrawerActivity.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.testapp;
+
+import android.app.Activity;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.support.v4.app.ActionBarDrawerToggle;
+import android.support.v4.widget.DrawerLayout;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+/**
+ * Activity to demonstrate actions on a {@link DrawerLayout}.
+ */
+public class DrawerActivity extends Activity {
+
+  public static final String[] DRAWER_CONTENTS =
+      new String[] {"Platypus", "Wombat", "Pickle", "Badger"};
+
+  private ActionBarDrawerToggle drawerToggle;
+  private CharSequence title;
+
+  @Override
+  public void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    setContentView(R.layout.drawer_activity);
+
+    ListAdapter listAdapter = new ArrayAdapter<String>(
+        getApplicationContext(), R.layout.drawer_row, R.id.drawer_row_name, DRAWER_CONTENTS);
+    final DrawerLayout drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
+    ListView drawerList = (ListView) findViewById(R.id.drawer_list);
+    drawerList.setAdapter(listAdapter);
+
+    final TextView textView = (TextView) findViewById(R.id.drawer_text_view);
+
+    drawerList.setOnItemClickListener(new OnItemClickListener() {
+      @Override
+      public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+        textView.setText("You picked: " + DRAWER_CONTENTS[(int) id]);
+        drawerLayout.closeDrawers();
+      }
+    });
+
+    // enable ActionBar app icon to behave as action to toggle nav drawer
+    // TODO(user): use compat lib for lower API levels
+    if (android.os.Build.VERSION.SDK_INT >= 11) {
+      getActionBar().setDisplayHomeAsUpEnabled(true);
+      getActionBar().setHomeButtonEnabled(true);
+    }
+
+    title = getTitle();
+
+    drawerToggle = new ActionBarDrawerToggle(
+        this,
+        drawerLayout,
+        R.drawable.ic_drawer,
+        R.string.nav_drawer_open,
+        R.string.nav_drawer_close) {
+
+        /** Called when a drawer has settled in a completely closed state. */
+        public void onDrawerClosed(View view) {
+          if (android.os.Build.VERSION.SDK_INT >= 11) {
+            getActionBar().setTitle(title);
+          }
+        }
+
+        /** Called when a drawer has settled in a completely open state. */
+        public void onDrawerOpened(View drawerView) {
+          if (android.os.Build.VERSION.SDK_INT >= 11) {
+            getActionBar().setTitle(title);
+          }
+        }
+    };
+    drawerLayout.setDrawerListener(drawerToggle);
+  }
+
+  @Override
+  public void setTitle(CharSequence title) {
+    this.title = title;
+    if (android.os.Build.VERSION.SDK_INT >= 11) {
+      getActionBar().setTitle(title);
+    }
+  }
+
+  @Override
+  public boolean onOptionsItemSelected(MenuItem item) {
+    // The action bar home/up action should open or close the drawer.
+    // ActionBarDrawerToggle will take care of this.
+    if (drawerToggle.onOptionsItemSelected(item)) {
+      return true;
+    }
+    return super.onOptionsItemSelected(item);
+  }
+
+  @Override
+  protected void onPostCreate(Bundle savedInstanceState) {
+    super.onPostCreate(savedInstanceState);
+    // Sync the toggle state after onRestoreInstanceState has occurred.
+    drawerToggle.syncState();
+  }
+
+  @Override
+  public void onConfigurationChanged(Configuration newConfig) {
+    super.onConfigurationChanged(newConfig);
+    // Pass any configuration change to the drawer toggls
+    drawerToggle.onConfigurationChanged(newConfig);
+  }
+}
diff --git a/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/FragmentStack.java b/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/FragmentStack.java
new file mode 100644
index 0000000..e89ce27
--- /dev/null
+++ b/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/FragmentStack.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.testapp;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.FragmentTransaction;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.TextView;
+
+/**
+ * Displays a counter using fragments.
+ */
+public class FragmentStack extends Activity {
+  int stackLevel = 1;
+
+  @Override
+  protected void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    setContentView(R.layout.fragment_stack);
+
+    // Watch for button clicks.
+    Button button = (Button) findViewById(R.id.new_fragment);
+    button.setOnClickListener(new OnClickListener() {
+      @Override
+      public void onClick(View v) {
+        addFragmentToStack();
+      }
+    });
+
+    if (savedInstanceState == null) {
+      // Do first time initialization -- add initial fragment.
+      Fragment newFragment = CountingFragment.newInstance(stackLevel);
+      FragmentTransaction ft = getFragmentManager().beginTransaction();
+      ft.add(R.id.simple_fragment, newFragment).commit();
+    } else {
+      stackLevel = savedInstanceState.getInt("level");
+    }
+  }
+
+  @Override
+  public void onSaveInstanceState(Bundle outState) {
+    super.onSaveInstanceState(outState);
+    outState.putInt("level", stackLevel);
+  }
+
+
+  void addFragmentToStack() {
+    stackLevel++;
+
+    // Instantiate a new fragment.
+    Fragment newFragment = CountingFragment.newInstance(stackLevel);
+
+    // Add the fragment to the activity, pushing this transaction
+    // on to the back stack.
+    FragmentTransaction ft = getFragmentManager().beginTransaction();
+    ft.replace(R.id.simple_fragment, newFragment);
+    ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
+    ft.addToBackStack(null);
+    ft.commit();
+  }
+
+
+
+  /**
+   * A fragment that displays a number.
+   */
+  public static class CountingFragment extends Fragment {
+    int counter;
+
+    /**
+     * Create a new instance of CountingFragment, providing "num"
+     * as an argument.
+     */
+    static CountingFragment newInstance(int num) {
+      CountingFragment f = new CountingFragment();
+
+      // Supply num input as an argument.
+      Bundle args = new Bundle();
+      args.putInt("num", num);
+      f.setArguments(args);
+
+      return f;
+    }
+
+    /**
+     * When creating, retrieve this instance's number from its arguments.
+     */
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+      super.onCreate(savedInstanceState);
+      counter = getArguments() != null ? getArguments().getInt("num") : 1;
+    }
+
+    /**
+     * The Fragment's UI is just a simple text view showing its
+     * instance number.
+     */
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+        Bundle savedInstanceState) {
+      TextView text = new TextView(getActivity());
+      text.setText("Fragment #" + counter);
+      return text;
+    }
+  }
+
+}
diff --git a/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/GestureActivity.java b/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/GestureActivity.java
new file mode 100644
index 0000000..b2cbc32
--- /dev/null
+++ b/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/GestureActivity.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.testapp;
+
+import com.google.common.collect.Lists;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.os.SystemClock;
+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.gesture_area);
+    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) {
+        boolean res = simpleDetector.onTouchEvent(m);
+        if (-1 != touchDelay) {
+          Log.i(TAG, "sleeping for: " + touchDelay);
+          SystemClock.sleep(touchDelay);
+
+        }
+        return res;
+      }
+    });
+  }
+
+  private volatile long touchDelay = -1;
+
+  public void setTouchDelay(long touchDelay) {
+    this.touchDelay = touchDelay;
+  }
+
+  public void areaClicked(@SuppressWarnings("unused") 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.text_double_click);
+      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.text_click);
+      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.text_swipe);
+      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.text_long_click);
+    }
+
+    @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.text_click).setVisibility(View.GONE);
+    findViewById(R.id.text_long_click).setVisibility(View.GONE);
+    findViewById(R.id.text_swipe).setVisibility(View.GONE);
+    findViewById(R.id.text_double_click).setVisibility(View.GONE);
+  }
+}
diff --git a/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/LongListActivity.java b/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/LongListActivity.java
new file mode 100644
index 0000000..300c0ee
--- /dev/null
+++ b/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/LongListActivity.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.testapp;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ListAdapter;
+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 {
+
+  @VisibleForTesting
+  public static final String STR = "STR";
+  @VisibleForTesting
+  public static final String LEN = "LEN";
+  @VisibleForTesting
+  public static final String FOOTER = "FOOTER";
+
+  private List<Map<String, Object>> data = Lists.newArrayList();
+  private LayoutInflater layoutInflater;
+
+  @Override
+  public void onCreate(Bundle bundle) {
+    super.onCreate(bundle);
+    populateData();
+    setContentView(R.layout.list_activity);
+    ((TextView) findViewById(R.id.selection_row_value)).setText("");
+    ((TextView) findViewById(R.id.selection_column_value)).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};
+    layoutInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+    ListAdapter adapter = new SimpleAdapter(this, data, R.layout.list_item, from, to) {
+      @Override
+      public View getView(final int position, View convertView, ViewGroup parent) {
+        if (convertView == null) {
+          convertView = layoutInflater.inflate(R.layout.list_item, null);
+        }
+
+        TextView textViewOne = (TextView) convertView.findViewById(R.id.item_content);
+        if (textViewOne != null) {
+          textViewOne.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+              ((TextView) findViewById(R.id.selection_row_value)).setText(String.valueOf(position));
+              ((TextView) findViewById(R.id.selection_column_value)).setText("1");
+            }
+          });
+        }
+
+        TextView textViewTwo = (TextView) convertView.findViewById(R.id.item_size);
+        if (textViewTwo != null) {
+          textViewTwo.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+              ((TextView) findViewById(R.id.selection_row_value)).setText(String.valueOf(position));
+              ((TextView) findViewById(R.id.selection_column_value)).setText("2");
+            }
+          });
+        }
+        return super.getView(position, convertView, parent);
+      }
+    };
+
+    View footerView = layoutInflater.inflate(R.layout.list_item, listView, false);
+    ((TextView) footerView.findViewById(R.id.item_content)).setText("count:");
+    ((TextView) footerView.findViewById(R.id.item_size)).setText(String.valueOf(data.size()));
+    listView.addFooterView(footerView, FOOTER, true);
+
+    listView.setAdapter(adapter);
+    listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+      @Override
+      public void onItemClick(
+          AdapterView<?> unusedParent, View clickedView, int position, long id) {
+        ((TextView) findViewById(R.id.selection_column_value)).setText("");
+        ((TextView) findViewById(R.id.selection_row_value)).setText(String.valueOf(position));
+      }
+    });
+  }
+
+  public Map<String, Object> makeItem(int forRow) {
+    Map<String, Object> dataRow = Maps.newHashMap();
+    dataRow.put(STR, "item: " + forRow);
+    dataRow.put(LEN, ((String) dataRow.get(STR)).length());
+    return dataRow;
+  }
+
+  private void populateData() {
+    for (int i = 0; i < 100; i++) {
+      data.add(makeItem(i));
+    }
+  }
+
+}
diff --git a/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/MainActivity.java b/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/MainActivity.java
new file mode 100644
index 0000000..c5ad762
--- /dev/null
+++ b/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/MainActivity.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.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/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/MenuActivity.java b/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/MenuActivity.java
new file mode 100644
index 0000000..e893cea
--- /dev/null
+++ b/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/MenuActivity.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.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.text_context_menu));
+  }
+
+  @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.text_menu_result);
+    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.text_menu_result);
+    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.text_menu_result);
+      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.text_menu_result);
+      text.setText(item.getTitle());
+      return true;
+    }
+  }
+}
diff --git a/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/ScrollActivity.java b/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/ScrollActivity.java
new file mode 100644
index 0000000..864fb23
--- /dev/null
+++ b/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/ScrollActivity.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.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/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/SendActivity.java b/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/SendActivity.java
new file mode 100644
index 0000000..fe472e7
--- /dev/null
+++ b/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/SendActivity.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.testapp;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+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.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.ArrayAdapter;
+import android.widget.AutoCompleteTextView;
+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.enter_data_edit_text);
+    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.enter_data_response_text);
+          responseText.setText(editText.getText());
+          return true;
+        } else {
+          return false;
+        }
+      }
+    });
+
+    final EditText searchBox = (EditText) findViewById(R.id.search_box);
+    searchBox.setOnEditorActionListener(new TextView.OnEditorActionListener() {
+      @Override
+      public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+        if (actionId == EditorInfo.IME_ACTION_SEARCH) {
+          TextView result = (TextView) findViewById(R.id.search_result);
+          result.setText(getString(R.string.searching_for_label) + " " + v.getText());
+          result.setVisibility(View.VISIBLE);
+          InputMethodManager imm =
+              (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+          imm.hideSoftInputFromWindow(searchBox.getWindowToken(), 0);
+          return true;
+        }
+        return false;
+      }
+    });
+    AutoCompleteTextView autoComplete = (AutoCompleteTextView) findViewById(
+        R.id.auto_complete_text_view);
+    String [] completions = new String[] {
+      "Pacific Ocean", "Atlantic Ocean", "Indian Ocean", "Southern Ocean", "Artic Ocean",
+      "Mediterranean Sea", "Caribbean Sea", "South China Sea", "Bering Sea",
+      "Gulf of Mexico", "Okhotsk Sea", "East China Sea", "Hudson Bay", "Japan Sea",
+      "Andaman Sea", "North Sea", "Red Sea", "Baltic Sea" };
+    ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
+        android.R.layout.simple_dropdown_item_1line,
+        completions);
+    autoComplete.setAdapter(adapter);
+  }
+
+  /** Called when user clicks the Send button */
+  public void sendData(@SuppressWarnings("unused") View view) {
+    Intent intent = new Intent(this, DisplayActivity.class);
+    EditText editText = (EditText) findViewById(R.id.send_data_edit_text);
+    intent.putExtra(EXTRA_DATA, editText.getText().toString());
+    startActivity(intent);
+  }
+
+  public void sendDataToCall(@SuppressWarnings("unused") View view) {
+    Intent intentToCall = new Intent(Intent.ACTION_CALL);
+    EditText editText = (EditText) findViewById(R.id.send_data_to_call_edit_text);
+    String number = editText.getText().toString();
+    intentToCall.setData(Uri.parse("tel:" + number));
+    startActivity(intentToCall);
+  }
+
+  public void sendDataToBrowser(@SuppressWarnings("unused") View view) {
+    EditText editText = (EditText) findViewById(R.id.send_data_to_browser_edit_text);
+    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(@SuppressWarnings("unused") View view) {
+    Intent sendIntent = new Intent();
+    EditText editText = (EditText) findViewById(R.id.send_data_to_message_edit_text);
+    sendIntent.setAction(Intent.ACTION_SEND);
+    sendIntent.putExtra(Intent.EXTRA_TEXT, editText.getText().toString());
+    sendIntent.setType("text/plain");
+    startActivity(sendIntent);
+  }
+
+  public void clickToMarket(@SuppressWarnings("unused") View view) {
+    Intent marketIntent = new Intent(Intent.ACTION_VIEW);
+    EditText editText = (EditText) findViewById(R.id.send_to_market_data);
+    marketIntent.setData(Uri.parse(
+        "market://details?id=" + editText.getText().toString()));
+    startActivity(marketIntent);
+  }
+
+  public void clickToGesture(@SuppressWarnings("unused") View view) {
+    startActivity(new Intent(this, GestureActivity.class));
+  }
+
+  public void clickToScroll(@SuppressWarnings("unused") View view) {
+    startActivity(new Intent(this, ScrollActivity.class));
+  }
+
+  public void clickToList(@SuppressWarnings("unused") View view) {
+    startActivity(new Intent(this, LongListActivity.class));
+  }
+
+  public boolean showDialog(@SuppressWarnings("unused") 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(@SuppressWarnings("unused") 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(user): 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.phone_number);
+        textView.setText(data.getExtras().getString("phone"));
+      }
+    }
+  }
+}
diff --git a/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/SimpleActivity.java b/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/SimpleActivity.java
new file mode 100644
index 0000000..8af2747
--- /dev/null
+++ b/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/SimpleActivity.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.testapp;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+/**
+ * Simple activity used to demonstrate a simple Espresso test.
+ */
+public class SimpleActivity extends Activity implements OnItemSelectedListener{
+
+  static final String EXTRA_DATA = "com.google.android.apps.common.testing.ui.testapp.DATA";
+
+  @Override
+  public void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    setContentView(R.layout.simple_activity);
+
+    Spinner spinner = (Spinner) findViewById(R.id.spinner_simple);
+    ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this,
+        R.array.spinner_array, android.R.layout.simple_spinner_item);
+    adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+    spinner.setAdapter(adapter);
+    spinner.setOnItemSelectedListener(this);
+  }
+
+  public void simpleButtonClicked(View view) {
+    TextView textView = (TextView) findViewById(R.id.text_simple);
+    String message = "Hello Espresso!";
+    textView.setText(message);
+  }
+
+  /** Called when user clicks the Send button */
+  public void sendButtonClicked(@SuppressWarnings("unused") View view) {
+    Intent intent = new Intent(this, DisplayActivity.class);
+    EditText editText = (EditText) findViewById(R.id.sendtext_simple);
+    intent.putExtra(EXTRA_DATA, editText.getText().toString());
+    startActivity(intent);
+  }
+
+  public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
+    TextView textView = (TextView) findViewById(R.id.spinnertext_simple);
+    textView.setText(String.format("One %s a day!", parent.getItemAtPosition(pos)));
+  }
+
+  public void onNothingSelected(AdapterView<?> parent) {
+  }
+}
+
diff --git a/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/SimplePagerAdapter.java b/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/SimplePagerAdapter.java
new file mode 100644
index 0000000..42d9cf4
--- /dev/null
+++ b/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/SimplePagerAdapter.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.testapp;
+
+import android.graphics.Color;
+import android.support.v4.view.PagerAdapter;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+class SimplePagerAdapter extends PagerAdapter {
+
+  private static final int[] COLORS = {
+    Color.BLUE,
+    Color.RED,
+    Color.YELLOW,
+  };
+
+  private static final int NUM_PAGES = COLORS.length;
+
+  @Override
+  public int getCount() {
+    return NUM_PAGES;
+  }
+
+  @Override
+  public boolean isViewFromObject(View view, Object object) {
+    return view == object;
+  }
+
+  @Override
+  public int getItemPosition(Object object) {
+    return ((ViewGroup) ((View) object).getParent()).indexOfChild((View) object);
+  }
+
+  @Override
+  public Object instantiateItem(ViewGroup container, int position) {
+    LayoutInflater inflater = LayoutInflater.from(container.getContext());
+    View view = inflater.inflate(R.layout.pager_view, null);
+    ((TextView) view.findViewById(R.id.pager_content)).setText("Position #" + position);
+    view.setBackgroundColor(COLORS[position]);
+    container.addView(view);
+    return view;
+  }
+
+  @Override
+  public void destroyItem(ViewGroup container, int position, Object object) {
+    container.removeView((View) object);
+  }
+}
diff --git a/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/SimpleWebViewActivity.java b/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/SimpleWebViewActivity.java
new file mode 100644
index 0000000..9c844af
--- /dev/null
+++ b/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/SimpleWebViewActivity.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.testapp;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.webkit.WebSettings;
+import android.webkit.WebView;
+
+/**
+ * One big web view to play with.
+ */
+public class SimpleWebViewActivity extends Activity {
+  @Override
+  public void onCreate(Bundle icicle) {
+    super.onCreate(icicle);
+    WebView mainWebView = new WebView(this);
+    setContentView(mainWebView);
+    mainWebView.loadData(
+        "<html>" +
+        "<script>document.was_clicked = false</script>" +
+        "<body> " +
+        "<button style='height:1000px;width:1000px;' onclick='document.was_clicked = true'> " +
+        "I'm a button</button>" +
+        "</body> " +
+        "</html>", "text/html", null);
+    WebSettings settings = mainWebView.getSettings();
+    settings.setJavaScriptEnabled(true);
+  }
+}
diff --git a/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/SwipeActivity.java b/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/SwipeActivity.java
new file mode 100644
index 0000000..93d1c18
--- /dev/null
+++ b/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/SwipeActivity.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.testapp;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.v4.view.ViewPager;
+
+/**
+ * Activity to test swipe interactions.
+ */
+public class SwipeActivity extends Activity {
+
+  @Override
+  public void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    setContentView(R.layout.swipe_activity);
+
+    ((ViewPager) findViewById(R.id.small_pager)).setAdapter(new SimplePagerAdapter());
+    ((ViewPager) findViewById(R.id.overlapped_pager)).setAdapter(new SimplePagerAdapter());
+  }
+
+}
+
diff --git a/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/SyncActivity.java b/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/SyncActivity.java
new file mode 100644
index 0000000..6702390
--- /dev/null
+++ b/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/SyncActivity.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.testapp;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.view.View;
+import android.widget.TextView;
+
+import java.util.Random;
+
+/**
+ * Displays "hello world" with a random delay of 2 to 7s after the user clicks on a button. This is
+ * used to demonstrate how Espresso can synchronize with any part of your application, which may
+ * cause the application state to be unstable (e.g. a network call).
+ */
+public class SyncActivity extends Activity {
+
+  /**
+   * A server that returns a hello world string
+   */
+  public interface HelloWorldServer {
+    String getHelloWorld();
+  }
+
+  private HelloWorldServer helloWorldServer;
+  private TextView statusTextView;
+
+  @Override
+  protected void onCreate(Bundle icicle) {
+    super.onCreate(icicle);
+
+    setContentView(R.layout.sync_activity);
+
+    statusTextView = checkNotNull(((TextView) findViewById(R.id.status_text)));
+
+    setHelloWorldServer(new HelloWorldServer() {
+      @Override
+      public String getHelloWorld() {
+        Random rand = new Random();
+        SystemClock.sleep(rand.nextInt(5000) + 2000);
+        return getString(R.string.hello_world);
+      }
+    });
+  }
+
+  public void onRequestButtonClick(@SuppressWarnings("unused") View view) {
+    Thread t = new Thread() {
+      @Override
+      public void run() {
+        final String helloworld = helloWorldServer.getHelloWorld();
+        runOnUiThread(new Runnable() {
+          @Override
+          public void run() {
+            setStatus(helloworld);
+          }
+        });
+      }
+    };
+    t.start();
+  }
+
+  private void setStatus(String text) {
+    statusTextView.setText(text);
+  }
+
+  @VisibleForTesting
+  public HelloWorldServer getHelloWorldServer() {
+    return helloWorldServer;
+  }
+
+  @VisibleForTesting
+  public void setHelloWorldServer(HelloWorldServer helloWorldServer) {
+    this.helloWorldServer = helloWorldServer;
+  }
+}
diff --git a/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/ViewPagerActivity.java b/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/ViewPagerActivity.java
new file mode 100644
index 0000000..ec92db8
--- /dev/null
+++ b/espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/ViewPagerActivity.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.testapp;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.v4.view.ViewPager;
+
+/**
+ * Activity to demonstrate actions on a {@link ViewPager}.
+ */
+public class ViewPagerActivity extends Activity {
+
+  @Override
+  public void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    setContentView(R.layout.pager_activity);
+
+    final ViewPager pager = (ViewPager) findViewById(R.id.pager_layout);
+    pager.setAdapter(new SimplePagerAdapter());
+  }
+
+}
+
diff --git a/espresso/espresso-sample/src/main/res/drawable-hdpi/ic_action_calendar.png b/espresso/espresso-sample/src/main/res/drawable-hdpi/ic_action_calendar.png
new file mode 100644
index 0000000..c7bd88b
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/drawable-hdpi/ic_action_calendar.png
Binary files differ
diff --git a/espresso/espresso-sample/src/main/res/drawable-hdpi/ic_action_key.png b/espresso/espresso-sample/src/main/res/drawable-hdpi/ic_action_key.png
new file mode 100644
index 0000000..ff876a0
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/drawable-hdpi/ic_action_key.png
Binary files differ
diff --git a/espresso/espresso-sample/src/main/res/drawable-hdpi/ic_action_lock.png b/espresso/espresso-sample/src/main/res/drawable-hdpi/ic_action_lock.png
new file mode 100644
index 0000000..1c80686
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/drawable-hdpi/ic_action_lock.png
Binary files differ
diff --git a/espresso/espresso-sample/src/main/res/drawable-hdpi/ic_action_save.png b/espresso/espresso-sample/src/main/res/drawable-hdpi/ic_action_save.png
new file mode 100644
index 0000000..827355d
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/drawable-hdpi/ic_action_save.png
Binary files differ
diff --git a/espresso/espresso-sample/src/main/res/drawable-hdpi/ic_action_search.png b/espresso/espresso-sample/src/main/res/drawable-hdpi/ic_action_search.png
new file mode 100644
index 0000000..b826566
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/drawable-hdpi/ic_action_search.png
Binary files differ
diff --git a/espresso/espresso-sample/src/main/res/drawable-hdpi/ic_action_world.png b/espresso/espresso-sample/src/main/res/drawable-hdpi/ic_action_world.png
new file mode 100644
index 0000000..612a5f2
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/drawable-hdpi/ic_action_world.png
Binary files differ
diff --git a/espresso/espresso-sample/src/main/res/drawable-hdpi/ic_drawer.png b/espresso/espresso-sample/src/main/res/drawable-hdpi/ic_drawer.png
new file mode 100644
index 0000000..9691a6c
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/drawable-hdpi/ic_drawer.png
Binary files differ
diff --git a/espresso/espresso-sample/src/main/res/drawable-hdpi/ic_launcher.png b/espresso/espresso-sample/src/main/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 0000000..c1615e0
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/drawable-hdpi/ic_launcher.png
Binary files differ
diff --git a/espresso/espresso-sample/src/main/res/drawable-ldpi/ic_action_calendar.png b/espresso/espresso-sample/src/main/res/drawable-ldpi/ic_action_calendar.png
new file mode 100644
index 0000000..37a9d7a
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/drawable-ldpi/ic_action_calendar.png
Binary files differ
diff --git a/espresso/espresso-sample/src/main/res/drawable-ldpi/ic_action_key.png b/espresso/espresso-sample/src/main/res/drawable-ldpi/ic_action_key.png
new file mode 100644
index 0000000..8628a15
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/drawable-ldpi/ic_action_key.png
Binary files differ
diff --git a/espresso/espresso-sample/src/main/res/drawable-ldpi/ic_action_lock.png b/espresso/espresso-sample/src/main/res/drawable-ldpi/ic_action_lock.png
new file mode 100644
index 0000000..a29abbb
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/drawable-ldpi/ic_action_lock.png
Binary files differ
diff --git a/espresso/espresso-sample/src/main/res/drawable-ldpi/ic_action_save.png b/espresso/espresso-sample/src/main/res/drawable-ldpi/ic_action_save.png
new file mode 100644
index 0000000..a51c100
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/drawable-ldpi/ic_action_save.png
Binary files differ
diff --git a/espresso/espresso-sample/src/main/res/drawable-ldpi/ic_action_search.png b/espresso/espresso-sample/src/main/res/drawable-ldpi/ic_action_search.png
new file mode 100644
index 0000000..1b4aac6
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/drawable-ldpi/ic_action_search.png
Binary files differ
diff --git a/espresso/espresso-sample/src/main/res/drawable-ldpi/ic_action_world.png b/espresso/espresso-sample/src/main/res/drawable-ldpi/ic_action_world.png
new file mode 100644
index 0000000..c27143c
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/drawable-ldpi/ic_action_world.png
Binary files differ
diff --git a/espresso/espresso-sample/src/main/res/drawable-ldpi/ic_drawer.png b/espresso/espresso-sample/src/main/res/drawable-ldpi/ic_drawer.png
new file mode 100644
index 0000000..9691a6c
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/drawable-ldpi/ic_drawer.png
Binary files differ
diff --git a/espresso/espresso-sample/src/main/res/drawable-ldpi/ic_launcher.png b/espresso/espresso-sample/src/main/res/drawable-ldpi/ic_launcher.png
new file mode 100644
index 0000000..110987a
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/drawable-ldpi/ic_launcher.png
Binary files differ
diff --git a/espresso/espresso-sample/src/main/res/drawable-mdpi/ic_action_calendar.png b/espresso/espresso-sample/src/main/res/drawable-mdpi/ic_action_calendar.png
new file mode 100644
index 0000000..7cb5f27
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/drawable-mdpi/ic_action_calendar.png
Binary files differ
diff --git a/espresso/espresso-sample/src/main/res/drawable-mdpi/ic_action_key.png b/espresso/espresso-sample/src/main/res/drawable-mdpi/ic_action_key.png
new file mode 100644
index 0000000..6bb6ff3
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/drawable-mdpi/ic_action_key.png
Binary files differ
diff --git a/espresso/espresso-sample/src/main/res/drawable-mdpi/ic_action_lock.png b/espresso/espresso-sample/src/main/res/drawable-mdpi/ic_action_lock.png
new file mode 100644
index 0000000..ee9dea0
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/drawable-mdpi/ic_action_lock.png
Binary files differ
diff --git a/espresso/espresso-sample/src/main/res/drawable-mdpi/ic_action_save.png b/espresso/espresso-sample/src/main/res/drawable-mdpi/ic_action_save.png
new file mode 100644
index 0000000..eebcf21
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/drawable-mdpi/ic_action_save.png
Binary files differ
diff --git a/espresso/espresso-sample/src/main/res/drawable-mdpi/ic_action_search.png b/espresso/espresso-sample/src/main/res/drawable-mdpi/ic_action_search.png
new file mode 100644
index 0000000..3516c9e
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/drawable-mdpi/ic_action_search.png
Binary files differ
diff --git a/espresso/espresso-sample/src/main/res/drawable-mdpi/ic_action_world.png b/espresso/espresso-sample/src/main/res/drawable-mdpi/ic_action_world.png
new file mode 100644
index 0000000..f7e8dcf
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/drawable-mdpi/ic_action_world.png
Binary files differ
diff --git a/espresso/espresso-sample/src/main/res/drawable-mdpi/ic_drawer.png b/espresso/espresso-sample/src/main/res/drawable-mdpi/ic_drawer.png
new file mode 100644
index 0000000..2190a93
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/drawable-mdpi/ic_drawer.png
Binary files differ
diff --git a/espresso/espresso-sample/src/main/res/drawable-mdpi/ic_launcher.png b/espresso/espresso-sample/src/main/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 0000000..90f091c
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/drawable-mdpi/ic_launcher.png
Binary files differ
diff --git a/espresso/espresso-sample/src/main/res/drawable-xhdpi/ic_action_calendar.png b/espresso/espresso-sample/src/main/res/drawable-xhdpi/ic_action_calendar.png
new file mode 100644
index 0000000..d34b110
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/drawable-xhdpi/ic_action_calendar.png
Binary files differ
diff --git a/espresso/espresso-sample/src/main/res/drawable-xhdpi/ic_action_key.png b/espresso/espresso-sample/src/main/res/drawable-xhdpi/ic_action_key.png
new file mode 100644
index 0000000..31c6756
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/drawable-xhdpi/ic_action_key.png
Binary files differ
diff --git a/espresso/espresso-sample/src/main/res/drawable-xhdpi/ic_action_lock.png b/espresso/espresso-sample/src/main/res/drawable-xhdpi/ic_action_lock.png
new file mode 100644
index 0000000..63797e2
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/drawable-xhdpi/ic_action_lock.png
Binary files differ
diff --git a/espresso/espresso-sample/src/main/res/drawable-xhdpi/ic_action_save.png b/espresso/espresso-sample/src/main/res/drawable-xhdpi/ic_action_save.png
new file mode 100644
index 0000000..2e7c579
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/drawable-xhdpi/ic_action_save.png
Binary files differ
diff --git a/espresso/espresso-sample/src/main/res/drawable-xhdpi/ic_action_search.png b/espresso/espresso-sample/src/main/res/drawable-xhdpi/ic_action_search.png
new file mode 100644
index 0000000..3539eab
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/drawable-xhdpi/ic_action_search.png
Binary files differ
diff --git a/espresso/espresso-sample/src/main/res/drawable-xhdpi/ic_action_world.png b/espresso/espresso-sample/src/main/res/drawable-xhdpi/ic_action_world.png
new file mode 100644
index 0000000..e1b21d3
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/drawable-xhdpi/ic_action_world.png
Binary files differ
diff --git a/espresso/espresso-sample/src/main/res/drawable-xhdpi/ic_drawer.png b/espresso/espresso-sample/src/main/res/drawable-xhdpi/ic_drawer.png
new file mode 100644
index 0000000..e2dd13a
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/drawable-xhdpi/ic_drawer.png
Binary files differ
diff --git a/espresso/espresso-sample/src/main/res/drawable-xhdpi/ic_launcher.png b/espresso/espresso-sample/src/main/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..0f6604b
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/drawable-xhdpi/ic_launcher.png
Binary files differ
diff --git a/espresso/espresso-sample/src/main/res/layout/actionbar_activity.xml b/espresso/espresso-sample/src/main/res/layout/actionbar_activity.xml
new file mode 100644
index 0000000..41fa6fa
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/layout/actionbar_activity.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2014 The Android Open Source Project
+  ~
+  ~ 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.
+  -->
+
+<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" >
+
+    <Button
+        android:id="@+id/show_contextual_action_bar"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_horizontal"
+        android:text="@string/text_show" />
+
+    <Button
+        android:id="@+id/hide_contextual_action_bar"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_horizontal"
+        android:text="@string/text_hide" />
+
+    <TextView
+        android:id="@+id/text_action_bar_result"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_horizontal"
+        android:layout_marginTop="20dp"
+        android:text="@string/text_empty"
+        android:textAppearance="?android:attr/textAppearanceSmall" />
+
+</LinearLayout>
diff --git a/espresso/espresso-sample/src/main/res/layout/delegating_edit_text.xml b/espresso/espresso-sample/src/main/res/layout/delegating_edit_text.xml
new file mode 100644
index 0000000..8d4cb33
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/layout/delegating_edit_text.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2014 The Android Open Source Project
+  ~
+  ~ 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.
+  -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android" >
+
+    <EditText
+        android:id="@+id/delegate_edit_text"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:inputType="text"
+        android:singleLine="true" />
+
+    <TextView
+        android:id="@+id/edit_text_message"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:singleLine="false"
+        android:visibility="gone" />
+
+</merge>
diff --git a/espresso/espresso-sample/src/main/res/layout/display_activity.xml b/espresso/espresso-sample/src/main/res/layout/display_activity.xml
new file mode 100644
index 0000000..5781524
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/layout/display_activity.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2014 The Android Open Source Project
+  ~
+  ~ 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.
+  -->
+
+<!-- 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/display_title"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="10dp"
+        android:text="@string/display_title"
+        android:textAppearance="?android:attr/textAppearanceMedium" />
+
+    <TextView
+        android:id="@+id/display_data"
+        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>
diff --git a/espresso/espresso-sample/src/main/res/layout/drawer_activity.xml b/espresso/espresso-sample/src/main/res/layout/drawer_activity.xml
new file mode 100644
index 0000000..ea5532d
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/layout/drawer_activity.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2014 The Android Open Source Project
+  ~
+  ~ 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.
+  -->
+
+<android.support.v4.widget.DrawerLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/drawer_layout"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent" >
+
+    <!-- The main content view -->
+    <FrameLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" >
+    <TextView
+        android:id="@+id/drawer_text_view"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content" />
+    </FrameLayout>
+
+    <ListView
+        android:id="@+id/drawer_list"
+        android:layout_width="240dp"
+        android:layout_height="match_parent"
+        android:layout_gravity="start"
+        android:choiceMode="singleChoice"
+        android:divider="@android:color/transparent"
+        android:dividerHeight="0dp"
+        android:background="#111" />
+
+</android.support.v4.widget.DrawerLayout>
diff --git a/espresso/espresso-sample/src/main/res/layout/drawer_row.xml b/espresso/espresso-sample/src/main/res/layout/drawer_row.xml
new file mode 100644
index 0000000..f56a688
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/layout/drawer_row.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2014 The Android Open Source Project
+  ~
+  ~ 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.
+  -->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_gravity="fill_horizontal|center_vertical"
+                android:gravity="fill_horizontal"
+                android:minHeight="70dip"
+                android:orientation="horizontal" >
+
+    <TextView android:id="@+id/drawer_row_name"
+              android:layout_width="wrap_content"
+              android:layout_height="match_parent"
+              android:layout_alignParentLeft="true"
+              android:gravity="left|center_vertical"/>
+</RelativeLayout>
diff --git a/espresso/espresso-sample/src/main/res/layout/fragment_stack.xml b/espresso/espresso-sample/src/main/res/layout/fragment_stack.xml
new file mode 100644
index 0000000..0861f87
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/layout/fragment_stack.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2014 The Android Open Source Project
+  ~
+  ~ 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.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:gravity="center_horizontal"
+    android:orientation="vertical"
+    android:padding="4dip" >
+
+    <FrameLayout
+        android:id="@+id/simple_fragment"
+        android:layout_width="match_parent"
+        android:layout_height="0px"
+        android:layout_weight="1" >
+    </FrameLayout>
+
+    <Button
+        android:id="@+id/new_fragment"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_weight="0"
+        android:text="Create New Fragment" >
+
+        <requestFocus />
+    </Button>
+
+</LinearLayout>
diff --git a/espresso/espresso-sample/src/main/res/layout/gesture_activity.xml b/espresso/espresso-sample/src/main/res/layout/gesture_activity.xml
new file mode 100644
index 0000000..d2bf3f7
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/layout/gesture_activity.xml
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2014 The Android Open Source Project
+  ~
+  ~ 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.
+  -->
+
+<!-- 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/text_click"
+            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/text_long_click"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:gravity="center"
+            android:text="@string/text_long_click"
+            android:textAppearance="?android:attr/textAppearanceMedium"
+            android:visibility="gone" />
+
+        <TextView
+            android:id="@+id/text_swipe"
+            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/text_double_click"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:gravity="center"
+            android:text="@string/text_double_click"
+            android:textAppearance="?android:attr/textAppearanceMedium"
+            android:visibility="gone" />
+    </LinearLayout>
+
+    <View
+        android:id="@+id/gesture_area"
+        android:layout_width="fill_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1"
+        android:clickable="true"
+        android:gravity="top"
+        android:onClick="areaClicked" />
+
+</LinearLayout>
diff --git a/espresso/espresso-sample/src/main/res/layout/list_activity.xml b/espresso/espresso-sample/src/main/res/layout/list_activity.xml
new file mode 100644
index 0000000..20dfc5c
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/layout/list_activity.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2014 The Android Open Source Project
+  ~
+  ~ 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.
+  -->
+
+<!-- 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"
+    android:paddingBottom="48dp"
+    android:paddingTop="48dp" >
+
+    <LinearLayout
+        android:layout_width="fill_parent"
+        android:layout_height="24dp" >
+
+        <TextView
+            android:id="@+id/selection_row"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/row_label" />
+
+        <TextView
+            android:id="@+id/selection_row_value"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="12dp" />
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="fill_parent"
+        android:layout_height="24dp" >
+
+        <TextView
+            android:id="@+id/selection_column"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/column_label" />
+
+        <TextView
+            android:id="@+id/selection_column_value"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="12dp" />
+    </LinearLayout>
+
+    <ListView
+        android:id="@+id/list"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content" />
+
+</LinearLayout>
diff --git a/espresso/espresso-sample/src/main/res/layout/list_item.xml b/espresso/espresso-sample/src/main/res/layout/list_item.xml
new file mode 100644
index 0000000..d1cf1d9
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/layout/list_item.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2014 The Android Open Source Project
+  ~
+  ~ 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.
+  -->
+
+<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/espresso/espresso-sample/src/main/res/layout/menu_activity.xml b/espresso/espresso-sample/src/main/res/layout/menu_activity.xml
new file mode 100644
index 0000000..f47e76b
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/layout/menu_activity.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2014 The Android Open Source Project
+  ~
+  ~ 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.
+  -->
+
+<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/popup_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginRight="10dp"
+            android:onClick="showPopup"
+            android:text="Click here for popup menu!" />
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:gravity="center_horizontal"
+        android:orientation="horizontal" >
+
+        <TextView
+            android:id="@+id/text_context_menu"
+            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/text_menu_result"
+        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>
diff --git a/espresso/espresso-sample/src/main/res/layout/pager_activity.xml b/espresso/espresso-sample/src/main/res/layout/pager_activity.xml
new file mode 100644
index 0000000..015b2fb
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/layout/pager_activity.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2014 The Android Open Source Project
+  ~
+  ~ 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.
+  -->
+
+<android.support.v4.view.ViewPager xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/pager_layout"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent" >
+
+</android.support.v4.view.ViewPager>
diff --git a/espresso/espresso-sample/src/main/res/layout/pager_view.xml b/espresso/espresso-sample/src/main/res/layout/pager_view.xml
new file mode 100644
index 0000000..0e8a802
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/layout/pager_view.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2014 The Android Open Source Project
+  ~
+  ~ 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.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent" >
+
+    <TextView
+        android:id="@+id/pager_content"
+        android:layout_width="fill_parent"
+        android:layout_height="fill_parent"
+        android:gravity="center" />
+
+</LinearLayout>
diff --git a/espresso/espresso-sample/src/main/res/layout/popup_window.xml b/espresso/espresso-sample/src/main/res/layout/popup_window.xml
new file mode 100644
index 0000000..f596ecb
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/layout/popup_window.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2014 The Android Open Source Project
+  ~
+  ~ 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.
+  -->
+
+<!--   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/espresso/espresso-sample/src/main/res/layout/scroll_activity.xml b/espresso/espresso-sample/src/main/res/layout/scroll_activity.xml
new file mode 100644
index 0000000..ecfee52
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/layout/scroll_activity.xml
@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2014 The Android Open Source Project
+  ~
+  ~ 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.
+  -->
+
+<!--   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/top_left"
+            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/double_scroll"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginTop="200dp"
+                    android:text="double_scroll"
+                    android:textColor="#000000" />
+            </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:layout_marginTop="3000dp"
+            android:background="#FFDDDDDD" >
+
+            <LinearLayout
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:orientation="horizontal" >
+
+                <TextView
+                    android:id="@+id/bottom_left"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="bottom_left"
+                    android:textColor="#000000" />
+
+                <TextView
+                    android:id="@+id/bottom_right"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginLeft="3000dp"
+                    android:text="bottom_right"
+                    android:textColor="#000000" />
+            </LinearLayout>
+        </HorizontalScrollView>
+    </LinearLayout>
+
+</ScrollView>
diff --git a/espresso/espresso-sample/src/main/res/layout/send_activity.xml b/espresso/espresso-sample/src/main/res/layout/send_activity.xml
new file mode 100644
index 0000000..2e67143
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/layout/send_activity.xml
@@ -0,0 +1,313 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2014 The Android Open Source Project
+  ~
+  ~ 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.
+  -->
+
+<!--
+  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/send_title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="10dp"
+            android:clickable="true"
+            android:onClick="sendData"
+            android:text="@string/send_title"
+            android:textAppearance="?android:attr/textAppearanceLarge" />
+
+        <EditText
+            android:id="@+id/send_data_edit_text"
+            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/send_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="10dp"
+            android:onClick="sendData"
+            android:text="@string/button_send" />
+
+        <EditText
+            android:id="@+id/enter_data_edit_text"
+            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/enter_data_response_text"
+            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/send_data_to_call_edit_text"
+            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/send_to_call_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="10dp"
+            android:onClick="sendDataToCall"
+            android:text="@string/button_call" />
+
+        <TextView
+            android:id="@+id/send_data_message"
+            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/send_data_to_message_edit_text"
+            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/send_message_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="10dp"
+            android:onClick="sendMessage"
+            android:text="@string/button_to_message" />
+
+        <TextView
+            android:id="@+id/goto_browser"
+            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/send_data_to_browser_edit_text"
+            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/send_to_browser_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="10dp"
+            android:onClick="sendDataToBrowser"
+            android:text="@string/button_to_browser" />
+
+        <TextView
+            android:id="@+id/pick_contact_title"
+            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/pick_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="10dp"
+            android:onClick="pickContact"
+            android:text="@string/button_pick" />
+
+        <TextView
+            android:id="@+id/phone_number"
+            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/send_to_market_data"
+            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/send_to_market_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="10dp"
+            android:onClick="clickToMarket"
+            android:text="@string/button_market" />
+
+        <EditText
+            android:id="@+id/search_box"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="10dp"
+            android:ems="10"
+            android:hint="search box"
+            android:imeOptions="actionSearch"
+            android:inputType="text" />
+
+        <TextView
+            android:id="@+id/search_result"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="10dp"
+            android:visibility="invisible" />
+
+        <TextView
+            android:id="@+id/weird_text_title"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="10dp"
+            android:text="Delegating Edit Text"
+            android:textAppearance="?android:attr/textAppearanceLarge" />
+
+        <com.google.android.apps.common.testing.ui.testapp.DelegatingEditText
+            android:id="@+id/delegating_edit_text"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="10dp" />
+
+        <TextView
+            android:id="@+id/gesture_title"
+            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/go_to_gesture_activity"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="10dp"
+            android:onClick="clickToGesture"
+            android:text="@string/button_gesture" />
+
+        <Button
+            android:id="@+id/scroll_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="10dp"
+            android:onClick="clickToScroll"
+            android:text="@string/launch_scroll_activity" />
+
+        <Button
+            android:id="@+id/list_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="10dp"
+            android:onClick="clickToList"
+            android:text="@string/launch_list_activity" />
+
+        <Button
+            android:id="@+id/make_alert_dialog"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="10dp"
+            android:onClick="showDialog"
+            android:text="@string/make_alert_dialog_button" />
+
+        <Button
+            android:id="@+id/make_popup_menu_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="10dp"
+            android:onClick="showPopupMenu"
+            android:text="@string/make_popup_menu_button" />
+
+        <Button
+            android:id="@+id/make_popup_view_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="10dp"
+            android:onClick="showPopupView"
+            android:text="@string/make_popup_view_button" />
+
+        <AutoCompleteTextView
+            android:id="@+id/auto_complete_text_view"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="10dp"
+            android:completionThreshold="1"
+            android:hint="@string/pick_water"
+            android:textAppearance="?android:attr/textAppearanceLarge" />
+
+
+        <!-- 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/bottom_send_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="1000dp"
+            android:onClick="sendData"
+            android:text="@string/button_send_bottom" />
+
+        <TextView
+            android:id="@+id/bottom_send_text_view"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="1000dp"
+            android:clickable="true"
+            android:onClick="sendData"
+            android:text="@string/send_title" />
+    </LinearLayout>
+
+</ScrollView>
diff --git a/espresso/espresso-sample/src/main/res/layout/simple_activity.xml b/espresso/espresso-sample/src/main/res/layout/simple_activity.xml
new file mode 100644
index 0000000..31aa760
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/layout/simple_activity.xml
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2014 The Android Open Source Project
+  ~
+  ~ 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.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical" >
+
+    <Spinner
+        android:id="@+id/spinner_simple"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content" />
+
+    <TextView
+        android:id="@+id/spinnertext_simple"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_horizontal"
+        android:text=""
+        android:textAppearance="?android:attr/textAppearanceLarge" />
+
+    <View
+        android:layout_width="1dp"
+        android:layout_height="30dp" />
+
+    <Button
+        android:id="@+id/button_simple"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_horizontal"
+        android:onClick="simpleButtonClicked"
+        android:text="@string/button_simple" />
+
+    <TextView
+        android:id="@+id/text_simple"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_horizontal"
+        android:text="@string/text_simple"
+        android:textAppearance="?android:attr/textAppearanceLarge" />
+
+    <View
+        android:layout_width="1dp"
+        android:layout_height="30dp" />
+
+    <EditText
+        android:id="@+id/sendtext_simple"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_horizontal"
+        android:ems="10"
+        android:hint="@string/send_hint" />
+
+    <Button
+        android:id="@+id/send_simple"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_horizontal"
+        android:onClick="sendButtonClicked"
+        android:text="@string/button_send" />
+
+</LinearLayout>
diff --git a/espresso/espresso-sample/src/main/res/layout/swipe_activity.xml b/espresso/espresso-sample/src/main/res/layout/swipe_activity.xml
new file mode 100644
index 0000000..e7bfa76
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/layout/swipe_activity.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2014 The Android Open Source Project
+  ~
+  ~ 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.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical" >
+
+  <!-- With update to rev 19, swipe does not consistently work on small pagers
+       b/12113054 opened to investigate why this regressed .-->
+  <android.support.v4.view.ViewPager
+      android:id="@+id/small_pager"
+      android:layout_width="120dp"
+      android:layout_height="48dp" />
+
+  <RelativeLayout
+      android:layout_width="fill_parent"
+      android:layout_height="200dp">
+    <android.support.v4.view.ViewPager
+        android:id="@+id/overlapped_pager"
+        android:layout_width="fill_parent"
+        android:layout_height="fill_parent"
+        android:layout_alignParentTop="true"
+        android:layout_alignParentLeft="true"/>
+    <TextView
+        android:layout_width="50dp"
+        android:layout_height="fill_parent"
+        android:background="#CCCCCC"
+        android:layout_alignParentTop="true"
+        android:layout_alignParentLeft="true"/>
+  </RelativeLayout>
+
+  <TextView
+      android:id="@+id/text_simple"
+      android:text="@string/text_simple"
+      android:layout_width="fill_parent"
+      android:layout_height="wrap_content"/>
+
+</LinearLayout>
diff --git a/espresso/espresso-sample/src/main/res/layout/sync_activity.xml b/espresso/espresso-sample/src/main/res/layout/sync_activity.xml
new file mode 100644
index 0000000..5642eee
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/layout/sync_activity.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2014 The Android Open Source Project
+  ~
+  ~ 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.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical" >
+
+    <Button
+        android:id="@+id/request_button"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:onClick="onRequestButtonClick"
+        android:text="@string/request_hello_world" />
+
+    <TextView
+        android:id="@+id/status_text"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:gravity="center"
+        android:textAppearance="?android:attr/textAppearanceMedium" />
+
+</LinearLayout>
+
diff --git a/espresso/espresso-sample/src/main/res/menu/actionbar_activity_actions.xml b/espresso/espresso-sample/src/main/res/menu/actionbar_activity_actions.xml
new file mode 100644
index 0000000..0358ffe
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/menu/actionbar_activity_actions.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2014 The Android Open Source Project
+  ~
+  ~ 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.
+  -->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:yourapp="http://schemas.android.com/apk/res-auto" >
+
+    <item
+        android:id="@+id/action_lock"
+        android:icon="@drawable/ic_action_lock"
+        android:title="Lock"
+        yourapp:showAsAction="ifRoom|withText"/>
+    <item
+        android:id="@+id/action_key"
+        android:icon="@drawable/ic_action_key"
+        android:title="Key"
+        yourapp:showAsAction="never"/>
+    <item
+        android:id="@+id/action_calendar"
+        android:icon="@drawable/ic_action_calendar"
+        android:title="Calendar"
+        yourapp:showAsAction="never"/>
+
+</menu>
diff --git a/espresso/espresso-sample/src/main/res/menu/actionbar_context_actions.xml b/espresso/espresso-sample/src/main/res/menu/actionbar_context_actions.xml
new file mode 100644
index 0000000..59233bb
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/menu/actionbar_context_actions.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2014 The Android Open Source Project
+  ~
+  ~ 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.
+  -->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:yourapp="http://schemas.android.com/apk/res-auto" >
+
+    <item
+        android:id="@+id/action_save"
+        android:icon="@drawable/ic_action_save"
+        android:title="Save"
+        yourapp:showAsAction="ifRoom|withText"/>
+    <item
+        android:id="@+id/action_search"
+        android:icon="@drawable/ic_action_search"
+        android:title="Search"
+        yourapp:showAsAction="never"/>
+    <item
+        android:id="@+id/action_world"
+        android:icon="@drawable/ic_action_world"
+        android:title="World"
+        yourapp:showAsAction="never"/>
+
+</menu>
diff --git a/espresso/espresso-sample/src/main/res/menu/contextmenu.xml b/espresso/espresso-sample/src/main/res/menu/contextmenu.xml
new file mode 100644
index 0000000..5d4137d
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/menu/contextmenu.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2014 The Android Open Source Project
+  ~
+  ~ 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.
+  -->
+
+<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>
diff --git a/espresso/espresso-sample/src/main/res/menu/optionsmenu.xml b/espresso/espresso-sample/src/main/res/menu/optionsmenu.xml
new file mode 100644
index 0000000..66ed1b2
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/menu/optionsmenu.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2014 The Android Open Source Project
+  ~
+  ~ 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.
+  -->
+
+<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>
diff --git a/espresso/espresso-sample/src/main/res/menu/popup_menu.xml b/espresso/espresso-sample/src/main/res/menu/popup_menu.xml
new file mode 100644
index 0000000..9bfb67f
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/menu/popup_menu.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2014 The Android Open Source Project
+  ~
+  ~ 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.
+  -->
+
+<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/espresso/espresso-sample/src/main/res/menu/popupmenu.xml b/espresso/espresso-sample/src/main/res/menu/popupmenu.xml
new file mode 100644
index 0000000..0dae632
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/menu/popupmenu.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2014 The Android Open Source Project
+  ~
+  ~ 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.
+  -->
+
+<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>
diff --git a/espresso/espresso-sample/src/main/res/values/strings.xml b/espresso/espresso-sample/src/main/res/values/strings.xml
new file mode 100644
index 0000000..e9d9ec5
--- /dev/null
+++ b/espresso/espresso-sample/src/main/res/values/strings.xml
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2014 The Android Open Source Project
+  ~
+  ~ 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.
+  -->
+
+<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="button_simple">Click Me!</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_long_click">Long Click</string>
+    <string name="text_swipe">Swipe</string>
+    <string name="text_double_click">Double Click</string>
+    <string name="text_empty"></string>
+    <string name="text_show">Show context actionbar</string>
+    <string name="text_hide">Hide context actionbar</string>
+    <string name="text_simple">Message</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>
+    <string name="searching_for_label">Searching for:</string>
+    <string name="row_label">clicked on row:</string>
+    <string name="column_label">clicked on column:</string>
+    <string name="hello_world">hello world!</string>
+    <string name="request_hello_world">Request hello world</string>
+    <string name="nav_drawer_open">Open navigation drawer</string>
+    <string name="pick_water">Pick a body of water</string>
+    <string name="nav_drawer_close">Close navigation drawer</string>
+
+    <string-array name="spinner_array">
+        <item>Espresso</item>
+        <item>Doppio</item>
+        <item>Macchiato</item>
+        <item>Cappuccino</item>
+        <item>Americano</item>
+        <item>Mocha</item>
+        <item>Late</item>
+    </string-array>
+
+</resources>
diff --git a/espresso/gradle.properties b/espresso/gradle.properties
new file mode 100644
index 0000000..bd11c51
--- /dev/null
+++ b/espresso/gradle.properties
@@ -0,0 +1,52 @@
+#
+# Copyright (C) 2014 The Android Open Source Project
+#
+# 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.
+#
+
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Settings specified in this file will override any Gradle settings
+# configured through the IDE.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+# Default value: -Xmx10248m -XX:MaxPermSize=256m
+# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+
+# Specifies custom SDK location
+#androidCustomSdkPath=/path/to/sdk
+
+##SNPSHOT??
+VERSION=1.2
+GROUP_ID=com.google.android.apps.common.testing //just espresso-lib?
+
+POM_DESCRIPTION=A simple API for writing reliable UI tests
+POM_URL=
+POM_SCM_URL=
+POM_SCM_CONNECTION=
+POM_SCM_DEV_CONNECTION=
+POM_LICENCE_NAME=The Apache Software License, Version 2.0
+POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt
+POM_LICENCE_DIST=repo
+POM_DEVELOPER_ID=
+POM_DEVELOPER_NAME=The Android Open Source Project
\ No newline at end of file
diff --git a/espresso/gradle/wrapper/gradle-wrapper.jar b/espresso/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..d5c591c
--- /dev/null
+++ b/espresso/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/espresso/gradle/wrapper/gradle-wrapper.properties b/espresso/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..f057df0
--- /dev/null
+++ b/espresso/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Jan 24 18:15:27 SGT 2014
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=http\://services.gradle.org/distributions/gradle-1.10-all.zip
diff --git a/espresso/gradlew b/espresso/gradlew
new file mode 100755
index 0000000..91a7e26
--- /dev/null
+++ b/espresso/gradlew
@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+    echo "$*"
+}
+
+die ( ) {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+esac
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched.
+if $cygwin ; then
+    [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+fi
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >&-
+APP_HOME="`pwd -P`"
+cd "$SAVED" >&-
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=$((i+1))
+    done
+    case $i in
+        (0) set -- ;;
+        (1) set -- "$args0" ;;
+        (2) set -- "$args0" "$args1" ;;
+        (3) set -- "$args0" "$args1" "$args2" ;;
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+    JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/espresso/gradlew.bat b/espresso/gradlew.bat
new file mode 100644
index 0000000..aec9973
--- /dev/null
+++ b/espresso/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off

+@rem ##########################################################################

+@rem

+@rem  Gradle startup script for Windows

+@rem

+@rem ##########################################################################

+

+@rem Set local scope for the variables with windows NT shell

+if "%OS%"=="Windows_NT" setlocal

+

+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.

+set DEFAULT_JVM_OPTS=

+

+set DIRNAME=%~dp0

+if "%DIRNAME%" == "" set DIRNAME=.

+set APP_BASE_NAME=%~n0

+set APP_HOME=%DIRNAME%

+

+@rem Find java.exe

+if defined JAVA_HOME goto findJavaFromJavaHome

+

+set JAVA_EXE=java.exe

+%JAVA_EXE% -version >NUL 2>&1

+if "%ERRORLEVEL%" == "0" goto init

+

+echo.

+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.

+echo.

+echo Please set the JAVA_HOME variable in your environment to match the

+echo location of your Java installation.

+

+goto fail

+

+:findJavaFromJavaHome

+set JAVA_HOME=%JAVA_HOME:"=%

+set JAVA_EXE=%JAVA_HOME%/bin/java.exe

+

+if exist "%JAVA_EXE%" goto init

+

+echo.

+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%

+echo.

+echo Please set the JAVA_HOME variable in your environment to match the

+echo location of your Java installation.

+

+goto fail

+

+:init

+@rem Get command-line arguments, handling Windowz variants

+

+if not "%OS%" == "Windows_NT" goto win9xME_args

+if "%@eval[2+2]" == "4" goto 4NT_args

+

+:win9xME_args

+@rem Slurp the command line arguments.

+set CMD_LINE_ARGS=

+set _SKIP=2

+

+:win9xME_args_slurp

+if "x%~1" == "x" goto execute

+

+set CMD_LINE_ARGS=%*

+goto execute

+

+:4NT_args

+@rem Get arguments from the 4NT Shell from JP Software

+set CMD_LINE_ARGS=%$

+

+:execute

+@rem Setup the command line

+

+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar

+

+@rem Execute Gradle

+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%

+

+:end

+@rem End local scope for the variables with windows NT shell

+if "%ERRORLEVEL%"=="0" goto mainEnd

+

+:fail

+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of

+rem the _cmd.exe /c_ return code!

+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1

+exit /b 1

+

+:mainEnd

+if "%OS%"=="Windows_NT" endlocal

+

+:omega

diff --git a/espresso/idling-resource-interface/build.gradle b/espresso/idling-resource-interface/build.gradle
new file mode 100644
index 0000000..24781e3
--- /dev/null
+++ b/espresso/idling-resource-interface/build.gradle
@@ -0,0 +1,17 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * 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.
+ */
+
+apply plugin: 'java'
\ No newline at end of file
diff --git a/espresso/idling-resource-interface/src/main/java/com/google/android/apps/common/testing/ui/espresso/IdlingResource.java b/espresso/idling-resource-interface/src/main/java/com/google/android/apps/common/testing/ui/espresso/IdlingResource.java
new file mode 100644
index 0000000..0f5e839
--- /dev/null
+++ b/espresso/idling-resource-interface/src/main/java/com/google/android/apps/common/testing/ui/espresso/IdlingResource.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.apps.common.testing.ui.espresso;
+
+/**
+ * Represents a resource of an application under test which can cause asynchronous background work
+ * to happen during test execution (e.g. an intent service that processes a button click). By
+ * default, {@link Espresso} synchronizes all view operations with the UI thread as well as
+ * AsyncTasks; however, it has no way of doing so with "hand-made" resources. In such cases, test
+ * authors can register the custom resource and {@link Espresso} will wait for the resource to
+ * become idle prior to executing a view operation.
+ * <br><br>
+ * <b>Important Note:</b> it is assumed that the resource stays idle most of the time.
+ */
+public interface IdlingResource {
+
+  /**
+   * Returns the name of the resources (used for logging and idempotency  of registration).
+   */
+  public String getName();
+
+  /**
+   * Returns {@code true} if resource is currently idle. Espresso will <b>always</b> call this
+   * method from the main thread, therefore it should be non-blocking and return immediately.
+   */
+  public boolean isIdleNow();
+
+  /**
+   * Registers the given {@link ResourceCallback} with the resource. Espresso will call this method:
+   * <ul>
+   * <li>with its implementation of {@link ResourceCallback} so it can be notified asynchronously
+   * that your resource is idle
+   * <li>from the main thread, but you are free to execute the callback's onTransitionToIdle from
+   * any thread
+   * <li>once (when it is initially given a reference to your IdlingResource)
+   * </ul>
+   * <br>
+   * You only need to call this upon transition from busy to idle - if the resource is already idle
+   * when the method is called invoking the call back is optional and has no significant impact.
+   */
+  public void registerIdleTransitionCallback(ResourceCallback callback);
+
+  /**
+   * Registered by an {@link IdlingResource} to notify Espresso of a transition to idle.
+   */
+  public interface ResourceCallback {
+    /**
+     * Called when the resource goes from busy to idle.
+     */
+    public void onTransitionToIdle();
+  }
+}
diff --git a/espresso/libs/README b/espresso/libs/README
new file mode 100644
index 0000000..b829d43
--- /dev/null
+++ b/espresso/libs/README
@@ -0,0 +1,18 @@
+The following outlines the license and the download location of the binary files
+present in this "libs" folder.
+
+dagger-1.2.1:
+jar: http://mvnrepository.com/artifact/com.squareup.dagger/dagger/1.2.1
+license: Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+
+dagger-compiler-1.2.1:
+jar: http://mvnrepository.com/artifact/com.squareup.dagger/dagger-compiler/1.2.1
+license: Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+
+guava-14.0.1
+jar: http://mvnrepository.com/artifact/com.google.guava/guava/14.0.1
+license: Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+
+jarjar-1.4
+jar: https://code.google.com/p/jarjar
+license: Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
\ No newline at end of file
diff --git a/espresso/libs/dagger-1.2.1.jar b/espresso/libs/dagger-1.2.1.jar
new file mode 100644
index 0000000..90d9509
--- /dev/null
+++ b/espresso/libs/dagger-1.2.1.jar
Binary files differ
diff --git a/espresso/libs/dagger-compiler-1.2.1.jar b/espresso/libs/dagger-compiler-1.2.1.jar
new file mode 100644
index 0000000..f2aa56d
--- /dev/null
+++ b/espresso/libs/dagger-compiler-1.2.1.jar
Binary files differ
diff --git a/espresso/libs/guava-14.0.1.jar b/espresso/libs/guava-14.0.1.jar
new file mode 100644
index 0000000..3a3d925
--- /dev/null
+++ b/espresso/libs/guava-14.0.1.jar
Binary files differ
diff --git a/espresso/libs/jarjar-1.4.jar b/espresso/libs/jarjar-1.4.jar
new file mode 100644
index 0000000..68b9db9
--- /dev/null
+++ b/espresso/libs/jarjar-1.4.jar
Binary files differ
diff --git a/espresso/libs/testrunner-1.1.jar b/espresso/libs/testrunner-1.1.jar
new file mode 100644
index 0000000..5abc79f
--- /dev/null
+++ b/espresso/libs/testrunner-1.1.jar
Binary files differ
diff --git a/espresso/libs/testrunner-runtime-1.1.jar b/espresso/libs/testrunner-runtime-1.1.jar
new file mode 100644
index 0000000..17b20d5
--- /dev/null
+++ b/espresso/libs/testrunner-runtime-1.1.jar
Binary files differ
diff --git a/espresso/publishLocal.gradle b/espresso/publishLocal.gradle
new file mode 100644
index 0000000..91596fa
--- /dev/null
+++ b/espresso/publishLocal.gradle
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * 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.
+ */
+
+apply plugin: 'maven'
+
+def getReleaseRepositoryUrl() {
+    if (hasProperty('androidSdkPath')) {
+        return "file:/$project.androidCustomSdkPath/extras/$project.POM_ARTIFACT_ID/m2repository"
+    } else {
+        println "No Android SDK path set. Using default m2 location, " +
+                "defined Maven settings.xml. Set ANDROID_HOME or set SDK location " +
+                "via ANDROID_SDK in gradle.properties"
+    }
+}
+
+task publishLocal(type: Upload) {
+//task publishLocal {
+//    configuration = configurations.archives
+//
+//    repositories {
+//        mavenCentral()
+//    }
+
+    uploadArchives {
+        repositories {
+            mavenDeployer {
+
+                println "***** ${getReleaseRepositoryUrl()}"
+                repository(url: getReleaseRepositoryUrl())
+
+                println "***** versoin $VERSION"
+                pom.project {
+                    pom.version = VERSION
+                    pom.groupId = GROUP_ID
+                    pom.artifactId = POM_ARTIFACT_ID
+
+//                    licenses {
+//                        license {
+//                            name POM_LICENCE_NAME
+//                            url POM_LICENCE_URL
+//                            distribution POM_LICENCE_DIST
+//                        }
+//                    }
+//
+//                    developers {
+//                        developer {
+//                            //id POM_DEVELOPER_ID
+//                            name POM_DEVELOPER_NAME
+//                        }
+//                    }
+                }
+            }
+        }
+    }
+
+//    def isReleaseBuild() {
+//        return VERSION.contains("SNAPSHOT") == false
+//    }
+//    signing {
+//        required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") }
+//        sign configurations.archives
+//    }
+//
+//    task androidJavadocs(type: Javadoc) {
+//        source = android.sourceSets.main.allJava
+//        classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
+//    }
+//
+//    task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) {
+//        classifier = 'javadoc'
+//        from androidJavadocs.destinationDir
+//    }
+//
+//    task androidSourcesJar(type: Jar) {
+//        classifier = 'sources'
+//        from android.sourceSets.main.allSource
+//    }
+//
+//    artifacts {
+//        archives androidSourcesJar
+//        archives androidJavadocsJar
+//    }
+}
diff --git a/espresso/settings.gradle b/espresso/settings.gradle
new file mode 100644
index 0000000..2a8f5f0
--- /dev/null
+++ b/espresso/settings.gradle
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * 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.
+ */
+
+include ':espresso-lib'
+include ':espresso-lib-tests'
+
+include ':espresso-contrib'
+include ':espresso-contrib-tests'
+
+include ':espresso-sample'
+
+include ':idling-resource-interface'
diff --git a/support/src/android/support/test/internal/runner/listener/SuiteAssignmentPrinter.java b/support/src/android/support/test/internal/runner/listener/SuiteAssignmentPrinter.java
index 798e252..aafe4d5 100644
--- a/support/src/android/support/test/internal/runner/listener/SuiteAssignmentPrinter.java
+++ b/support/src/android/support/test/internal/runner/listener/SuiteAssignmentPrinter.java
@@ -35,7 +35,7 @@
      * This constant defines the maximum allowed runtime (in ms) for a test included in the "small"
      * suite. It is used to make an educated guess at what suite an unlabeled test belongs to.
      */
-    private static final float SMALL_SUITE_MAX_RUNTIME = 100;
+    private static final float SMALL_SUITE_MAX_RUNTIME = 200;
 
     /**
      * This constant defines the maximum allowed runtime (in ms) for a test included in the "medium"
@@ -61,6 +61,10 @@
         if (!mTimingValid || mStartTime < 0) {
             assignmentSuite = "NA";
             runTime = -1;
+            sendString("F");
+            Log.d("SuiteAssignmentPrinter", String.format(
+                    "%s#%s: skipping suite assignment due to test failure\n", description.getClassName(),
+                    description.getMethodName()));
         } else {
             runTime = endTime - mStartTime;
             if (runTime < SMALL_SUITE_MAX_RUNTIME) {
@@ -70,23 +74,23 @@
             } else {
                 assignmentSuite = TestRequestBuilder.LARGE_SIZE;
             }
+
+            String currentSize = getTestSize(description);
+            if (!assignmentSuite.equals(currentSize)) {
+                // test size != runtime
+                sendString(String.format("\n%s#%s: current size: %s. suggested: %s runTime: %d ms\n",
+                        description.getClassName(), description.getMethodName(), currentSize,
+                        assignmentSuite, runTime));
+            } else {
+                sendString(".");
+                Log.d("SuiteAssignmentPrinter", String.format(
+                        "%s#%s assigned correctly as %s. runTime: %d ms\n", description.getClassName(),
+                        description.getMethodName(), assignmentSuite, runTime));
+            }
         }
         // Clear mStartTime so that we can verify that it gets set next time.
         mStartTime = -1;
 
-        String currentSize = getTestSize(description);
-        if (!assignmentSuite.equals(currentSize)) {
-            // test size != runtime
-            sendString(String.format("\n%s#%s: current size: %s. suggested: %s runTime: %d ms\n",
-                    description.getClassName(), description.getMethodName(), currentSize,
-                    assignmentSuite, runTime));
-        } else {
-            sendString(".");
-            Log.d("SuiteAssignmentPrinter", String.format(
-                    "%s#%s assigned correctly as %s. runTime: %d ms\n", description.getClassName(),
-                    description.getMethodName(), assignmentSuite, runTime));
-        }
-
     }
 
     private String getTestSize(Description description) {