summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNick Korostelev <nkorsote@google.com>2014-06-17 22:46:55 +0000
committerAndroid (Google) Code Review <android-gerrit@google.com>2014-06-17 20:42:58 +0000
commit69848b5928ae5eabac7e13e5f9ccb6e31ad3bde7 (patch)
treea3504ffa401fd0dc40e0a157e479eba8d3b8842f
parent63726971b42c7a6fb9df9d70117ad10511da96e3 (diff)
parentf69eb9ac2856f470cb79f57141f711ed3ceed99d (diff)
downloadtesting-69848b5928ae5eabac7e13e5f9ccb6e31ad3bde7.tar.gz
Merge "port Espresso to Android repo"
-rw-r--r--espresso/build.gradle41
-rw-r--r--espresso/espresso-contrib-tests/build.gradle54
-rw-r--r--espresso/espresso-contrib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/contrib/DrawerActionsIntegrationTest.java106
-rw-r--r--espresso/espresso-contrib/build.gradle43
-rw-r--r--espresso/espresso-contrib/src/main/AndroidManifest.xml26
-rw-r--r--espresso/espresso-contrib/src/main/java/com/google/android/apps/common/testing/ui/espresso/contrib/DrawerActions.java275
-rw-r--r--espresso/espresso-contrib/src/main/java/com/google/android/apps/common/testing/ui/espresso/contrib/DrawerMatchers.java74
-rw-r--r--espresso/espresso-lib-tests/build.gradle57
-rw-r--r--espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/AmbiguousViewMatcherExceptionTest.java90
-rw-r--r--espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/AppNotIdleExceptionTest.java77
-rw-r--r--espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/EspressoEdgeCaseTest.java308
-rw-r--r--espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/EspressoTest.java152
-rw-r--r--espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/NoMatchingViewExceptionTest.java64
-rw-r--r--espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/UnitTests.java34
-rwxr-xr-xespresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/ViewInteractionTest.java208
-rw-r--r--espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/AdapterDataIntegrationTest.java93
-rw-r--r--espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/ClearTextActionIntegrationTest.java63
-rw-r--r--espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/EditorActionIntegrationTest.java79
-rw-r--r--espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/EspressoKeyBuilderTest.java77
-rw-r--r--espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/EventActionIntegrationTest.java118
-rw-r--r--espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/GeneralLocationTest.java109
-rw-r--r--espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/KeyEventActionIntegrationTest.java151
-rw-r--r--espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/ScrollToActionIntegrationTest.java76
-rw-r--r--espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/SwipeActionIntegrationTest.java110
-rw-r--r--espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/TypeTextActionIntegrationTest.java97
-rw-r--r--espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/TypeTextActionTest.java93
-rw-r--r--espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/action/WindowOrderingIntegrationTest.java95
-rw-r--r--espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/assertion/ViewAssertionsTest.java170
-rw-r--r--espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/base/AsyncTaskPoolMonitorTest.java179
-rw-r--r--espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/base/DefaultFailureHandlerTest.java116
-rw-r--r--espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/base/EventInjectorTest.java207
-rw-r--r--espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/base/IdlingResourceIntegrationTest.java127
-rw-r--r--espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/base/IdlingResourceRegistryTest.java237
-rw-r--r--espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/base/OnDemandIdlingResource.java59
-rw-r--r--espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/base/UiControllerImplIntegrationTest.java295
-rw-r--r--espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/base/UiControllerImplTest.java390
-rw-r--r--espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/base/ViewFinderImplTest.java112
-rw-r--r--espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/contrib/CountingIdlingResourceTest.java120
-rw-r--r--espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/matcher/PreferenceMatchersTest.java84
-rw-r--r--espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/matcher/ViewMatchersTest.java456
-rw-r--r--espresso/espresso-lib-tests/src/androidTest/java/com/google/android/apps/common/testing/ui/espresso/util/TreeIterablesTest.java221
-rw-r--r--espresso/espresso-lib-tests/src/androidTest/res/values/id.xml23
-rw-r--r--espresso/espresso-lib-tests/src/androidTest/res/values/strings.xml22
-rw-r--r--espresso/espresso-lib/build.gradle112
-rw-r--r--espresso/espresso-lib/gradle.properties21
-rw-r--r--espresso/espresso-lib/src/main/AndroidManifest.xml26
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/AmbiguousViewMatcherException.java140
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/AndroidManifest.xml27
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/AppNotIdleException.java75
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/DataInteraction.java207
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/Espresso.java255
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/EspressoException.java22
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/FailureHandler.java37
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/GraphHolder.java78
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/IdlingPolicies.java100
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/IdlingPolicy.java125
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/IdlingResourceTimeoutException.java37
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/InjectEventSecurityException.java36
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/NoActivityResumedException.java31
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/NoMatchingRootException.java41
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/NoMatchingViewException.java136
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/PerformException.java86
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/Root.java87
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/UiController.java83
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/ViewAction.java68
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/ViewAssertion.java52
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/ViewFinder.java36
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/ViewInteraction.java178
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/ViewInteractionModule.java69
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/AdapterDataLoaderAction.java156
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/AdapterViewProtocol.java196
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/AdapterViewProtocols.java158
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/ClearTextAction.java50
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/CloseKeyboardAction.java140
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/CoordinatesProvider.java33
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/EditorAction.java79
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/EspressoKey.java111
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/GeneralClickAction.java141
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/GeneralLocation.java112
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/GeneralSwipeAction.java108
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/KeyEventAction.java125
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/MotionEvents.java249
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/PrecisionDescriber.java30
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/Press.java46
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/ScrollToAction.java79
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/Swipe.java115
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/Swiper.java55
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/Tap.java108
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/Tapper.java54
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/TypeTextAction.java127
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/action/ViewActions.java223
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/assertion/ViewAssertions.java142
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/AsyncTaskPoolMonitor.java208
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/BaseLayerModule.java167
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/CompatAsyncTask.java29
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/Default.java30
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/DefaultFailureHandler.java96
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/EventInjectionStrategy.java50
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/EventInjector.java99
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/IdlingResourceRegistry.java289
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/InputManagerEventInjectionStrategy.java162
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/LooperIdlingResource.java130
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/MainThread.java29
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/QueueInterrogator.java169
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/RootViewPicker.java217
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/RootsOracle.java167
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/SdkAsyncTask.java29
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/ThreadPoolExecutorExtractor.java154
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/UiControllerImpl.java535
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/ViewFinderImpl.java133
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/base/WindowManagerEventInjectionStrategy.java150
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/contrib/CountingIdlingResource.java210
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/matcher/BoundedMatcher.java77
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/matcher/PreferenceMatchers.java185
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/matcher/RootMatchers.java217
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/matcher/ViewMatchers.java809
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/util/HumanReadables.java201
-rw-r--r--espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/util/TreeIterables.java300
-rw-r--r--espresso/espresso-sample/build.gradle47
-rw-r--r--espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/ActionBarTest.java105
-rw-r--r--espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/AdapterViewTest.java142
-rw-r--r--espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/AdvancedSynchronizationTest.java91
-rw-r--r--espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/BasicTest.java100
-rw-r--r--espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/CustomFailureHandlerTest.java92
-rw-r--r--espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/DrawerActionsTest.java87
-rw-r--r--espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/LongListMatchers.java127
-rw-r--r--espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/LongListMatchersTest.java57
-rw-r--r--espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/MenuTest.java78
-rw-r--r--espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/MultipleWindowTest.java122
-rw-r--r--espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/ScrollToTest.java56
-rw-r--r--espresso/espresso-sample/src/androidTest/java/com/google/android/apps/common/testing/ui/testapp/SwipeTest.java96
-rw-r--r--espresso/espresso-sample/src/main/AndroidManifest.xml47
-rw-r--r--espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/ActionBarTestActivity.java100
-rw-r--r--espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/DelegatingEditText.java63
-rw-r--r--espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/DisplayActivity.java36
-rw-r--r--espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/DrawerActivity.java129
-rw-r--r--espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/FragmentStack.java127
-rw-r--r--espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/GestureActivity.java224
-rw-r--r--espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/LongListActivity.java128
-rw-r--r--espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/MainActivity.java112
-rw-r--r--espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/MenuActivity.java98
-rw-r--r--espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/ScrollActivity.java32
-rw-r--r--espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/SendActivity.java219
-rw-r--r--espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/SimpleActivity.java72
-rw-r--r--espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/SimplePagerAdapter.java65
-rw-r--r--espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/SimpleWebViewActivity.java44
-rw-r--r--espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/SwipeActivity.java38
-rw-r--r--espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/SyncActivity.java95
-rw-r--r--espresso/espresso-sample/src/main/java/com/google/android/apps/common/testing/ui/testapp/ViewPagerActivity.java38
-rw-r--r--espresso/espresso-sample/src/main/res/drawable-hdpi/ic_action_calendar.pngbin0 -> 334 bytes
-rw-r--r--espresso/espresso-sample/src/main/res/drawable-hdpi/ic_action_key.pngbin0 -> 500 bytes
-rw-r--r--espresso/espresso-sample/src/main/res/drawable-hdpi/ic_action_lock.pngbin0 -> 321 bytes
-rw-r--r--espresso/espresso-sample/src/main/res/drawable-hdpi/ic_action_save.pngbin0 -> 318 bytes
-rw-r--r--espresso/espresso-sample/src/main/res/drawable-hdpi/ic_action_search.pngbin0 -> 618 bytes
-rw-r--r--espresso/espresso-sample/src/main/res/drawable-hdpi/ic_action_world.pngbin0 -> 1143 bytes
-rw-r--r--espresso/espresso-sample/src/main/res/drawable-hdpi/ic_drawer.pngbin0 -> 2843 bytes
-rw-r--r--espresso/espresso-sample/src/main/res/drawable-hdpi/ic_launcher.pngbin0 -> 2751 bytes
-rw-r--r--espresso/espresso-sample/src/main/res/drawable-ldpi/ic_action_calendar.pngbin0 -> 236 bytes
-rw-r--r--espresso/espresso-sample/src/main/res/drawable-ldpi/ic_action_key.pngbin0 -> 293 bytes
-rw-r--r--espresso/espresso-sample/src/main/res/drawable-ldpi/ic_action_lock.pngbin0 -> 206 bytes
-rw-r--r--espresso/espresso-sample/src/main/res/drawable-ldpi/ic_action_save.pngbin0 -> 235 bytes
-rw-r--r--espresso/espresso-sample/src/main/res/drawable-ldpi/ic_action_search.pngbin0 -> 318 bytes
-rw-r--r--espresso/espresso-sample/src/main/res/drawable-ldpi/ic_action_world.pngbin0 -> 524 bytes
-rw-r--r--espresso/espresso-sample/src/main/res/drawable-ldpi/ic_drawer.pngbin0 -> 2843 bytes
-rw-r--r--espresso/espresso-sample/src/main/res/drawable-ldpi/ic_launcher.pngbin0 -> 1398 bytes
-rw-r--r--espresso/espresso-sample/src/main/res/drawable-mdpi/ic_action_calendar.pngbin0 -> 241 bytes
-rw-r--r--espresso/espresso-sample/src/main/res/drawable-mdpi/ic_action_key.pngbin0 -> 358 bytes
-rw-r--r--espresso/espresso-sample/src/main/res/drawable-mdpi/ic_action_lock.pngbin0 -> 242 bytes
-rw-r--r--espresso/espresso-sample/src/main/res/drawable-mdpi/ic_action_save.pngbin0 -> 254 bytes
-rw-r--r--espresso/espresso-sample/src/main/res/drawable-mdpi/ic_action_search.pngbin0 -> 418 bytes
-rw-r--r--espresso/espresso-sample/src/main/res/drawable-mdpi/ic_action_world.pngbin0 -> 710 bytes
-rw-r--r--espresso/espresso-sample/src/main/res/drawable-mdpi/ic_drawer.pngbin0 -> 2838 bytes
-rw-r--r--espresso/espresso-sample/src/main/res/drawable-mdpi/ic_launcher.pngbin0 -> 1787 bytes
-rw-r--r--espresso/espresso-sample/src/main/res/drawable-xhdpi/ic_action_calendar.pngbin0 -> 334 bytes
-rw-r--r--espresso/espresso-sample/src/main/res/drawable-xhdpi/ic_action_key.pngbin0 -> 603 bytes
-rw-r--r--espresso/espresso-sample/src/main/res/drawable-xhdpi/ic_action_lock.pngbin0 -> 396 bytes
-rw-r--r--espresso/espresso-sample/src/main/res/drawable-xhdpi/ic_action_save.pngbin0 -> 344 bytes
-rw-r--r--espresso/espresso-sample/src/main/res/drawable-xhdpi/ic_action_search.pngbin0 -> 782 bytes
-rw-r--r--espresso/espresso-sample/src/main/res/drawable-xhdpi/ic_action_world.pngbin0 -> 1599 bytes
-rw-r--r--espresso/espresso-sample/src/main/res/drawable-xhdpi/ic_drawer.pngbin0 -> 2855 bytes
-rw-r--r--espresso/espresso-sample/src/main/res/drawable-xhdpi/ic_launcher.pngbin0 -> 3655 bytes
-rw-r--r--espresso/espresso-sample/src/main/res/layout/actionbar_activity.xml47
-rw-r--r--espresso/espresso-sample/src/main/res/layout/delegating_edit_text.xml34
-rw-r--r--espresso/espresso-sample/src/main/res/layout/display_activity.xml42
-rw-r--r--espresso/espresso-sample/src/main/res/layout/drawer_activity.xml44
-rw-r--r--espresso/espresso-sample/src/main/res/layout/drawer_row.xml31
-rw-r--r--espresso/espresso-sample/src/main/res/layout/fragment_stack.xml42
-rw-r--r--espresso/espresso-sample/src/main/res/layout/gesture_activity.xml80
-rw-r--r--espresso/espresso-sample/src/main/res/layout/list_activity.xml66
-rw-r--r--espresso/espresso-sample/src/main/res/layout/list_item.xml33
-rw-r--r--espresso/espresso-sample/src/main/res/layout/menu_activity.xml62
-rw-r--r--espresso/espresso-sample/src/main/res/layout/pager_activity.xml23
-rw-r--r--espresso/espresso-sample/src/main/res/layout/pager_view.xml28
-rw-r--r--espresso/espresso-sample/src/main/res/layout/popup_window.xml39
-rw-r--r--espresso/espresso-sample/src/main/res/layout/scroll_activity.xml93
-rw-r--r--espresso/espresso-sample/src/main/res/layout/send_activity.xml313
-rw-r--r--espresso/espresso-sample/src/main/res/layout/simple_activity.xml77
-rw-r--r--espresso/espresso-sample/src/main/res/layout/swipe_activity.xml54
-rw-r--r--espresso/espresso-sample/src/main/res/layout/sync_activity.xml38
-rw-r--r--espresso/espresso-sample/src/main/res/menu/actionbar_activity_actions.xml37
-rw-r--r--espresso/espresso-sample/src/main/res/menu/actionbar_context_actions.xml37
-rw-r--r--espresso/espresso-sample/src/main/res/menu/contextmenu.xml33
-rw-r--r--espresso/espresso-sample/src/main/res/menu/optionsmenu.xml33
-rw-r--r--espresso/espresso-sample/src/main/res/menu/popup_menu.xml27
-rw-r--r--espresso/espresso-sample/src/main/res/menu/popupmenu.xml33
-rw-r--r--espresso/espresso-sample/src/main/res/values/strings.xml90
-rw-r--r--espresso/gradle.properties52
-rw-r--r--espresso/gradle/wrapper/gradle-wrapper.jarbin0 -> 50508 bytes
-rw-r--r--espresso/gradle/wrapper/gradle-wrapper.properties6
-rwxr-xr-xespresso/gradlew164
-rw-r--r--espresso/gradlew.bat90
-rw-r--r--espresso/idling-resource-interface/build.gradle17
-rw-r--r--espresso/idling-resource-interface/src/main/java/com/google/android/apps/common/testing/ui/espresso/IdlingResource.java66
-rw-r--r--espresso/libs/README18
-rw-r--r--espresso/libs/dagger-1.2.1.jarbin0 -> 60490 bytes
-rw-r--r--espresso/libs/dagger-compiler-1.2.1.jarbin0 -> 68930 bytes
-rw-r--r--espresso/libs/guava-14.0.1.jarbin0 -> 2189117 bytes
-rw-r--r--espresso/libs/jarjar-1.4.jarbin0 -> 120575 bytes
-rw-r--r--espresso/libs/testrunner-1.1.jarbin0 -> 49060 bytes
-rw-r--r--espresso/libs/testrunner-runtime-1.1.jarbin0 -> 3279 bytes
-rw-r--r--espresso/publishLocal.gradle96
-rw-r--r--espresso/settings.gradle25
222 files changed, 20069 insertions, 0 deletions
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'