Add libhidl_test to native_test_list
am: 99295db9a6

Change-Id: Idaabce7a1ac6c1a86f9a7579805437f10971f3a3
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
new file mode 100644
index 0000000..94deaa7
--- /dev/null
+++ b/PREUPLOAD.cfg
@@ -0,0 +1,11 @@
+[Hook Scripts]
+checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
+                  -fw libraries/annotations
+                      libraries/aupt-lib
+                      libraries/base-app-helpers
+                      libraries/launcher-helper
+                      libraries/metrics-helper
+                      libraries/power-helper
+                      libraries/system-helpers
+                      libraries/timeresult-helpers
+
diff --git a/build/tasks/tests/instrumentation_metric_test_list.mk b/build/tasks/tests/instrumentation_metric_test_list.mk
index 8b14aed..3387458 100644
--- a/build/tasks/tests/instrumentation_metric_test_list.mk
+++ b/build/tasks/tests/instrumentation_metric_test_list.mk
@@ -14,7 +14,12 @@
 
 instrumentation_metric_tests := \
     crashcollector \
+    CorePerfTests \
     DocumentsUIPerfTests \
     DocumentsUIAppPerfTests \
+    MtpDocumentsProviderPerfTests \
     perf-setup.sh \
-    SurfaceComposition
+    SurfaceComposition \
+    RsBlasBenchmark \
+    ImageProcessingJB \
+    MultiUserPerfTests
diff --git a/build/tasks/tests/instrumentation_test_list.mk b/build/tasks/tests/instrumentation_test_list.mk
index 5b02b00..0ded95b 100644
--- a/build/tasks/tests/instrumentation_test_list.mk
+++ b/build/tasks/tests/instrumentation_test_list.mk
@@ -13,11 +13,14 @@
 # limitations under the License.
 
 instrumentation_tests := \
+    HelloWorldTests \
     crashcollector \
     ManagedProvisioningTests \
     FrameworksCoreTests \
+    FrameworksNetTests \
+    FrameworksNotificationTests \
     FrameworksServicesTests \
-    FrameworksUtilsTests \
+    FrameworksUtilTests \
     MtpDocumentsProviderTests \
     DocumentsUITests \
     ShellTests \
@@ -27,6 +30,7 @@
     FrameworksWifiTests \
     FrameworksTelephonyTests \
     ContactsProviderTests \
+    ContactsProviderTests2 \
     SettingsUnitTests \
     TelecomUnitTests \
     AndroidVCardTests \
@@ -39,5 +43,13 @@
     DownloadProviderTests \
     EmergencyInfoTests \
     CalendarProviderTests \
+    SettingsLibTests \
+    RetailDemoTests \
+    RSTest \
+    PrintSpoolerOutOfProcessTests \
+    CellBroadcastReceiverUnitTests \
     TelephonyProviderTests \
-    NetworkRecommendationTests
+    CarrierConfigTests \
+    TeleServiceTests \
+    SettingsProviderTest \
+    StorageManagerUnitTests
diff --git a/build/tasks/tests/native_metric_test_list.mk b/build/tasks/tests/native_metric_test_list.mk
index 1ca9567..3ec3c79 100644
--- a/build/tasks/tests/native_metric_test_list.mk
+++ b/build/tasks/tests/native_metric_test_list.mk
@@ -16,6 +16,10 @@
     binderAddInts \
     bionic-benchmarks \
     crashcollector \
+    hwuimacro \
+    hwuimicro \
     libjavacore-benchmarks \
+    minikin_perftests \
     mmapPerf \
+    netd_benchmark \
     perf-setup.sh
diff --git a/build/tasks/tests/native_test_list.mk b/build/tasks/tests/native_test_list.mk
index 8a91efe..08a6856 100644
--- a/build/tasks/tests/native_test_list.mk
+++ b/build/tasks/tests/native_test_list.mk
@@ -18,13 +18,20 @@
     bionic-unit-tests \
     bionic-unit-tests-static \
     bluetoothtbd_test \
+    bootstat_tests \
     boringssl_crypto_test \
     boringssl_ssl_test \
+    buffer_hub_queue-test \
+    buffer_hub_queue_producer-test \
     bugreportz_test \
-    camera2_test \
+    bsdiff_unittest \
     camera_client_test \
     crashcollector \
     debuggerd_test \
+    dumpstate_test \
+    dumpstate_test_fixture \
+    dumpsys_test \
+    hello_world_test \
     hwui_unit_tests \
     init_tests \
     JniInvocation_test \
@@ -36,6 +43,8 @@
     libhidl_test \
     libjavacore-unit-tests \
     liblog-unit-tests \
+    libminijail_unittest_gtest \
+    libtextclassifier_tests \
     libwifi-system_tests \
     linker-unit-tests \
     logcat-unit-tests \
@@ -56,10 +65,15 @@
     netd_unit_test \
     pagemap_test \
     perfprofd_test \
+    recovery_component_test \
+    recovery_unit_test \
+    scrape_mmap_addr \
     simpleperf_cpu_hotplug_test \
     simpleperf_unit_test \
+    syscall_filter_unittest_gtest \
     time-unit-tests \
     update_engine_unittests \
     wificond_unit_test \
     wifilogd_unit_test \
-    ziparchive-tests
+    ziparchive-tests \
+    SurfaceFlinger_test
diff --git a/libraries/annotations/Android.mk b/libraries/annotations/Android.mk
index 9c4c2e1..391a17e 100644
--- a/libraries/annotations/Android.mk
+++ b/libraries/annotations/Android.mk
@@ -19,6 +19,7 @@
 include $(CLEAR_VARS)
 LOCAL_MODULE := platform-test-annotations
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_SDK_VERSION := current  # To allow use from CTS
 include $(BUILD_STATIC_JAVA_LIBRARY)
 
 # Build for host side tests
diff --git a/libraries/annotations/src/android/platform/test/annotations/GlobalPresubmit.java b/libraries/annotations/src/android/platform/test/annotations/GlobalPresubmit.java
new file mode 100644
index 0000000..046afd5
--- /dev/null
+++ b/libraries/annotations/src/android/platform/test/annotations/GlobalPresubmit.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marks a test that should run as part of the global presubmit suite for platform development.
+ *
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.TYPE})
+public @interface GlobalPresubmit {
+
+}
diff --git a/libraries/annotations/src/android/platform/test/annotations/Presubmit.java b/libraries/annotations/src/android/platform/test/annotations/Presubmit.java
index 5f08acc..484847c 100644
--- a/libraries/annotations/src/android/platform/test/annotations/Presubmit.java
+++ b/libraries/annotations/src/android/platform/test/annotations/Presubmit.java
@@ -22,7 +22,7 @@
 import java.lang.annotation.Target;
 
 /**
- * Marks a test that should run as part of the presubmit suite for platform development.
+ * Marks a test or test class that should run as part of a project scoped presubmit suite
  *
  */
 @Retention(RetentionPolicy.RUNTIME)
diff --git a/libraries/annotations/src/android/platform/test/annotations/RequiresDevice.java b/libraries/annotations/src/android/platform/test/annotations/RequiresDevice.java
new file mode 100644
index 0000000..852582c
--- /dev/null
+++ b/libraries/annotations/src/android/platform/test/annotations/RequiresDevice.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Indicates that a specific host side test should not be run against an emulator.
+ * <p/>
+ * It will be executed only if the test is running against a physical android device. <br>
+ * For device side tests, annotate with android.support.test.filters.RequiresDevice
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.TYPE})
+public @interface RequiresDevice {
+}
diff --git a/libraries/base-app-helpers/Android.mk b/libraries/app-helpers/auto/Android.mk
similarity index 78%
copy from libraries/base-app-helpers/Android.mk
copy to libraries/app-helpers/auto/Android.mk
index 0111a0a..c6519fd 100644
--- a/libraries/base-app-helpers/Android.mk
+++ b/libraries/app-helpers/auto/Android.mk
@@ -1,5 +1,5 @@
 #
-# Copyright (C) 2016 The Android Open Source Project
+# Copyright (C) 2017 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -16,9 +16,8 @@
 LOCAL_PATH := $(call my-dir)
 
 include $(CLEAR_VARS)
-LOCAL_MODULE := base-app-helpers
-LOCAL_STATIC_JAVA_LIBRARIES := dpad-util
-LOCAL_JAVA_LIBRARIES := ub-uiautomator launcher-helper-lib
+LOCAL_MODULE := auto-app-helper-base
+LOCAL_STATIC_JAVA_LIBRARIES := app-helpers-common
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
 include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/app-helpers/auto/src/android/platform/test/helpers/auto/AbstractAutoDialHelper.java b/libraries/app-helpers/auto/src/android/platform/test/helpers/auto/AbstractAutoDialHelper.java
new file mode 100644
index 0000000..b0ceb28
--- /dev/null
+++ b/libraries/app-helpers/auto/src/android/platform/test/helpers/auto/AbstractAutoDialHelper.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.helpers;
+
+import android.app.Instrumentation;
+
+public abstract class AbstractAutoDialHelper extends AbstractStandardAppHelper{
+
+    public AbstractAutoDialHelper(Instrumentation instr) {
+    super(instr);
+  }
+
+    /**
+     * Setup expectations: The app is open.
+     *
+     * This method is used to enter a dial a number and make calls.
+     * @param phoneNumber - phone number to dial.
+     */
+    public abstract void dialNumber(String phoneNumber);
+
+    /**
+     * Setup expectations: The app is open.
+     *
+     * This method is used to end call.
+     */
+    public abstract void endCall();
+
+    /**
+     * Setup expectations: The app is open.
+     *
+     * This method is used to open call history details.
+     */
+    public abstract void openCallHistory();
+
+    /**
+     * Setup expectations: The app is open.
+     *
+     * This method is used to open missed call details.
+     */
+    public abstract void openMissedCall();
+
+}
diff --git a/libraries/app-helpers/auto/src/android/platform/test/helpers/auto/AbstractAutoMediaHelper.java b/libraries/app-helpers/auto/src/android/platform/test/helpers/auto/AbstractAutoMediaHelper.java
new file mode 100644
index 0000000..40c4fef
--- /dev/null
+++ b/libraries/app-helpers/auto/src/android/platform/test/helpers/auto/AbstractAutoMediaHelper.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.helpers;
+
+import android.app.Instrumentation;
+
+public abstract class AbstractAutoMediaHelper extends AbstractStandardAppHelper{
+
+    public AbstractAutoMediaHelper(Instrumentation instr) {
+        super(instr);
+    }
+
+    /**
+     * Setup expectations: media app is open
+     *
+     * This method is used to play media.
+     *
+     */
+    public abstract void playMedia();
+
+    /**
+     * Setup expectations: media app is open.
+     *
+     * This method is used to pause media.
+     *
+     */
+    public abstract void pauseMedia();
+
+    /**
+     * Setup expectations: media app is open.
+     *
+     * This method is used to select next track.
+     *
+     */
+    public abstract void clickNextTrack();
+
+    /**
+     * Setup expectations: media app is open.
+     *
+     * This method is used to select previous track.
+     *
+     */
+    public abstract void clickPreviousTrack();
+
+    /**
+     * Setup expectations: media app is open.
+     *
+     * This method is used to shuffle tracks.
+     *
+     */
+    public abstract void clickShuffleAll();
+
+    /**
+     * Setup expectations: media app is open.
+     *
+     * This method is used to open Folder Menu with menuOptions.
+     * Example - openMenu->Folder->Mediafilename->trackName
+     *           openMenuWith(Folder,mediafilename,trackName);
+     *
+     * @param - menuOptions used to pass multiple level of menu options in one go.
+     *
+     */
+    public abstract void openMenuWith(String... menuOptions);
+
+    /**
+     * Setup expectations: media app is open.
+     *
+     * This method is used to used to open mediafilename from now playing list.
+     *
+     *  @param - trackName - media to be played.
+     */
+    public abstract void openNowPlayingWith(String trackName);
+
+    /**
+     * Setup expectations: Radio app is open.
+     *
+     * @return to get current playing track name.
+     */
+    public abstract String getMediaTrackName();
+
+}
diff --git a/libraries/app-helpers/auto/src/android/platform/test/helpers/auto/AbstractAutoOverviewHelper.java b/libraries/app-helpers/auto/src/android/platform/test/helpers/auto/AbstractAutoOverviewHelper.java
new file mode 100644
index 0000000..e0cb1a5
--- /dev/null
+++ b/libraries/app-helpers/auto/src/android/platform/test/helpers/auto/AbstractAutoOverviewHelper.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.helpers;
+
+import android.app.Instrumentation;
+
+public abstract class AbstractAutoOverviewHelper extends AbstractStandardAppHelper{
+
+    public AbstractAutoOverviewHelper(Instrumentation instr) {
+        super(instr);
+    }
+
+    /**
+     * Setup expectations: in home (overview) screen.
+     *
+     * This method is used to open settings.
+     *
+     */
+    public abstract void openSettings();
+
+    /**
+     * Setup expectations: in home (overview) screen.
+     *
+     * This method is used to start voice assistant.
+     *
+     */
+    public abstract void startVoiceAssistant();
+
+    /**
+     * Setup expectations:
+     * 1.Play media from Media player/ Radio.
+     * 2.Select Home button.
+     * 3.Media/Radio card shown in home (overview) screen.
+     *
+     * else throws UnknownUiException if element not found.
+     *
+     * This method is used to click next track/next station on Media/Radio card.
+     *
+     */
+    public abstract void clickNextTrack();
+
+    /**
+     * Setup expectations:
+     * 1.Play media from Media player/ Radio.
+     * 2.Select Home button.
+     * 3.Media/Radio card shown in home (overview) screen.
+     *
+     * else throws UnknownUiException if element not found.
+     *
+     * This method is used to click previous track/previous station on Media/Radio card.
+     *
+     */
+    public abstract void clickPreviousTrack();
+
+    /**
+     * Setup expectations:
+     * 1.Play media from Media player/ Radio.
+     * 2.Select Home button.
+     * 3.Media/Radio card shown in home (overview) screen.
+     *
+     * else throws UnknownUiException if element not found.
+     *
+     * This method is used to play media on Media/Radio card.
+     *
+     */
+    public abstract void playMedia();
+
+    /**
+     * Setup expectations:
+     * 1.Play media from Media player/ Radio.
+     * 2.Select Home button.
+     * 3.Media/Radio card shown in home (overview) screen.
+     *
+     * else throws UnknownUiException if element not found.
+     *
+     * This method is used to pause media on Media/Radio card.
+     *
+     */
+    public abstract void pauseMedia();
+
+    /**
+     * Setup expectations:
+     * 1.Play media from Media player/ Radio.
+     * 2.Select Home button.
+     * 3.Media/Radio card shown in home (overview) screen.
+     *
+     * else throws UnknownUiException if element not found.
+     *
+     * This method is used to open active media card app.
+     * ( Radio,Bluetooth Media, Local Media player).
+     *
+     */
+    public abstract void openMediaApp();
+
+    /**
+     * Setup expectations:
+     * 1.Dial call from Dial app .
+     * 2.Select Home button.
+     * 3.Dial card shown in home (overview) screen.
+     *
+     * else throws UnknownUiException if element not found.
+     *
+     * This method is used to end call on Dial card.
+     *
+     */
+    public abstract void endCall();
+
+    /**
+     * Setup expectations:
+     * 1.Dial call from Dial app .
+     * 2.Select Home button.
+     * 3.Dial card shown in home (overview) screen.
+     *
+     * else throws UnknownUiException if element not found.
+     *
+     * This method is used to mute on going call.
+     *
+     */
+    public abstract void muteCall();
+
+    /**
+     * Setup expectations:
+     * 1.Dial call from Dial app and end call.
+     * 2.Select Home button.
+     * 3.Recent Dial card shown in home (overview) screen.
+     *
+     * else throws UnknownUiException if element not found.
+     *
+     * This method is used to dial recent call activity.
+     *
+     */
+    public abstract void dialRecentCall();
+
+}
diff --git a/libraries/app-helpers/auto/src/android/platform/test/helpers/auto/AbstractAutoRadioHelper.java b/libraries/app-helpers/auto/src/android/platform/test/helpers/auto/AbstractAutoRadioHelper.java
new file mode 100644
index 0000000..e782a17
--- /dev/null
+++ b/libraries/app-helpers/auto/src/android/platform/test/helpers/auto/AbstractAutoRadioHelper.java
@@ -0,0 +1,110 @@
+
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.helpers;
+
+import android.app.Instrumentation;
+
+public abstract class AbstractAutoRadioHelper extends AbstractStandardAppHelper{
+
+    public AbstractAutoRadioHelper(Instrumentation instr) {
+        super(instr);
+    }
+
+    /**
+     * Setup expectations: Radio app is open
+     *
+     * This method is used to play Radio.
+     *
+     */
+    public abstract void playRadio();
+
+    /**
+     * Setup expectations: Radio app is open.
+     *
+     * This method is used to pause radio.
+     *
+     */
+    public abstract void pauseRadio();
+
+    /**
+     * Setup expectations: Radio app is open.
+     *
+     * This method is used to select next station.
+     *
+     */
+    public abstract void clickNextStation();
+
+    /**
+     * Setup expectations: Radio app is open.
+     *
+     * This method is used to select previous station.
+     *
+     */
+    public abstract void clickPreviousStation();
+
+    /**
+     * Setup expectations: Radio app is open.
+     *
+     * This method is used to save current station.
+     *
+     */
+    public abstract void saveCurrentStation();
+
+    /**
+     * Setup expectations: Radio app is open.
+     *
+     * This method is used to open saved station list.
+     *
+     */
+    public abstract void openSavedStationList();
+
+    /**
+     * Setup expectations: Radio app is open.
+     *
+     * This method is used to select AM from menu.
+     *
+     */
+    public abstract void clickAmFromMenu();
+
+    /**
+     * Setup expectations: Radio app is open.
+     *
+     * This method is used to select FM from menu.
+     *
+     */
+    public abstract void clickFmFromMenu();
+
+    /**
+     * Setup expectations: Radio app is open.
+     *
+     * This method is used to tune station manually.
+     *
+     * @param stationType - to select AM or FM.
+     * @param band - band to tune in.
+     *
+     */
+    public abstract void setStation(String stationType,double band);
+
+    /**
+     * Setup expectations: Radio app is open.
+     *
+     * @return to get current playing station band with Am or Fm.
+     */
+    public abstract String getStationBand();
+
+}
diff --git a/libraries/app-helpers/auto/src/android/platform/test/helpers/auto/AbstractAutoUiProviderHelper.java b/libraries/app-helpers/auto/src/android/platform/test/helpers/auto/AbstractAutoUiProviderHelper.java
new file mode 100644
index 0000000..40b14e4
--- /dev/null
+++ b/libraries/app-helpers/auto/src/android/platform/test/helpers/auto/AbstractAutoUiProviderHelper.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.helpers;
+
+
+import android.app.Instrumentation;
+
+/**
+ * AbstractAutoUiProviderHelper used to open menu in different applications like Dial,
+ * Bluetooth Media and Local Media player.
+ */
+
+public abstract class AbstractAutoUiProviderHelper extends AbstractStandardAppHelper {
+
+    public AbstractAutoUiProviderHelper(Instrumentation instr) {
+    super(instr);
+  }
+
+    /**
+     * Setup expectations: The applications like dial,Media should be open.
+     *
+     * This method is used to open menu in different applications like Dial and Media.
+     */
+    public abstract void openMenu();
+
+}
diff --git a/libraries/base-app-helpers/Android.mk b/libraries/app-helpers/base/Android.mk
similarity index 79%
copy from libraries/base-app-helpers/Android.mk
copy to libraries/app-helpers/base/Android.mk
index 0111a0a..82785d1 100644
--- a/libraries/base-app-helpers/Android.mk
+++ b/libraries/app-helpers/base/Android.mk
@@ -16,9 +16,12 @@
 LOCAL_PATH := $(call my-dir)
 
 include $(CLEAR_VARS)
-LOCAL_MODULE := base-app-helpers
-LOCAL_STATIC_JAVA_LIBRARIES := dpad-util
-LOCAL_JAVA_LIBRARIES := ub-uiautomator launcher-helper-lib
+LOCAL_MODULE := app-helpers-base
+LOCAL_JAVA_LIBRARIES := ub-uiautomator android-support-test
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
 include $(BUILD_STATIC_JAVA_LIBRARY)
+
+######################################
+
+include $(call all-makefiles-under, $(LOCAL_PATH))
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/HelperManager.java b/libraries/app-helpers/base/src/android/platform/test/helpers/base/HelperManager.java
similarity index 85%
rename from libraries/base-app-helpers/src/android/platform/test/helpers/HelperManager.java
rename to libraries/app-helpers/base/src/android/platform/test/helpers/base/HelperManager.java
index 533b0b2..43215fe 100644
--- a/libraries/base-app-helpers/src/android/platform/test/helpers/HelperManager.java
+++ b/libraries/app-helpers/base/src/android/platform/test/helpers/base/HelperManager.java
@@ -16,10 +16,15 @@
 
 package android.platform.test.helpers;
 
+import static java.lang.reflect.Modifier.isAbstract;
+import static java.lang.reflect.Modifier.isInterface;
+
 import android.app.Instrumentation;
 import android.content.Context;
 import android.util.Log;
+
 import dalvik.system.DexFile;
+
 import java.io.File;
 import java.io.IOException;
 import java.lang.ClassLoader;
@@ -134,21 +139,46 @@
      * @throws RuntimeException if no implementation is found
      * @return a concrete implementation of base
      */
-    public <T extends AbstractStandardAppHelper> T get(Class<T> base) {
+    public <T extends IAppHelper> T get(Class<T> base) {
+        List<T> implementations = getAll(base);
+
+        if (implementations.size() > 0) {
+            return implementations.get(0);
+        } else {
+            throw new RuntimeException(
+                    String.format("Failed to find an implementation for %s", base.toString()));
+        }
+    }
+
+    /**
+     * Returns all concrete implementations of the helper interface supplied.
+     *
+     * @param base the interface base class to find an implementation for
+     * @return a list of all concrete implementations we could find
+     */
+    public <T extends IAppHelper> List<T> getAll(Class<T> base) {
         ClassLoader loader = HelperManager.class.getClassLoader();
+        List<T> implementations = new ArrayList<>();
+
         // Iterate and search for the implementation
         for (String className : mClasses) {
             Class<?> clazz = null;
             try {
                 clazz = loader.loadClass(className);
+                // Skip non-instantiable classes
+                if (isAbstract(clazz.getModifiers()) || isInterface(clazz.getModifiers())) {
+                    continue;
+                }
             } catch (ClassNotFoundException e) {
                 Log.w(LOG_TAG, String.format("Class not found: %s", className));
+                continue;
             }
             if (base.isAssignableFrom(clazz) && !clazz.equals(base)) {
+
                 // Instantiate the implementation class and return
                 try {
                     Constructor<?> constructor = clazz.getConstructor(Instrumentation.class);
-                    return (T)constructor.newInstance(mInstrumentation);
+                    implementations.add((T)constructor.newInstance(mInstrumentation));
                 } catch (NoSuchMethodException e) {
                     Log.w(LOG_TAG, String.format("Failed to find a matching constructor for %s",
                             className), e);
@@ -164,7 +194,7 @@
                 }
             }
         }
-        throw new RuntimeException(
-                String.format("Failed to find an implementation for %s", base.toString()));
+
+        return implementations;
     }
 }
diff --git a/tests/jank/notificationsgenerator_wear/src/com/google/android/wearable/support/CustomNotificationRemoteInputActivity.java b/libraries/app-helpers/base/src/android/platform/test/helpers/base/IAppHelper.java
similarity index 73%
copy from tests/jank/notificationsgenerator_wear/src/com/google/android/wearable/support/CustomNotificationRemoteInputActivity.java
copy to libraries/app-helpers/base/src/android/platform/test/helpers/base/IAppHelper.java
index b58f8df..5d312b0 100644
--- a/tests/jank/notificationsgenerator_wear/src/com/google/android/wearable/support/CustomNotificationRemoteInputActivity.java
+++ b/libraries/app-helpers/base/src/android/platform/test/helpers/base/IAppHelper.java
@@ -1,6 +1,5 @@
 /*
-
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2017 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -15,10 +14,7 @@
  * limitations under the License.
  */
 
-package com.google.android.wearable.support;
+package android.platform.test.helpers;
 
-import android.app.Activity;
-
-public class CustomNotificationRemoteInputActivity extends Activity{
-
+public interface IAppHelper {
 }
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/IStandardAppHelper.java b/libraries/app-helpers/base/src/android/platform/test/helpers/base/IStandardAppHelper.java
similarity index 60%
rename from libraries/base-app-helpers/src/android/platform/test/helpers/IStandardAppHelper.java
rename to libraries/app-helpers/base/src/android/platform/test/helpers/base/IStandardAppHelper.java
index 9658429..f23648f 100644
--- a/libraries/base-app-helpers/src/android/platform/test/helpers/IStandardAppHelper.java
+++ b/libraries/app-helpers/base/src/android/platform/test/helpers/base/IStandardAppHelper.java
@@ -18,7 +18,9 @@
 
 import android.content.pm.PackageManager.NameNotFoundException;
 
-public interface IStandardAppHelper {
+import java.io.IOException;
+
+public interface IStandardAppHelper extends IAppHelper {
 
     /**
      * Setup expectation: On the launcher home screen.
@@ -38,14 +40,14 @@
     /**
      * Setup expectations: This application is on the initial launch screen.
      * <p>
-     * This method will dismiss all visible relevant dialogs and block until this process is
-     * complete.
+     * Dismiss all visible relevant dialogs and block until this process is complete.
      */
     abstract void dismissInitialDialogs();
 
     /**
      * Setup expectations: None
      * <p>
+     * Get the target application's component package.
      * @return the package name for this helper's application.
      */
     abstract String getPackage();
@@ -53,24 +55,43 @@
     /**
      * Setup expectations: None.
      * <p>
-     * @return the name for this application in the launcher.
+     * Get the target application's launcher name.
+     * @return the name of this application's launcher.
      */
     abstract String getLauncherName();
 
     /**
      * Setup expectations: None
      * <p>
-     * This method will return the version String from PackageManager.
-     *
-     * @return the version as a String
-     * @throws NameNotFoundException if the package is not found in PM
+     * Get the target application's version String.
+     * @return the version code
+     * @throws NameNotFoundException if {@code getPackage} is not found
      */
     abstract String getVersion() throws NameNotFoundException;
 
     /**
      * Setup expectations: None
-     * <p>
      * @return true, if this app's package is the root (depth 0), and false otherwise
      */
     abstract boolean isAppInForeground();
+
+    /**
+     * Setup expectations: None
+     * <p>
+     * Captures a screenshot and UI XML with the supplied name.
+     * @param name the screenshot prefix
+     * @throws IOException if there is a capture failure
+     * @throws RuntimeException if creating the screenshot directory fails.
+     */
+    abstract boolean captureScreenshot(String name) throws IOException;
+
+    /**
+     * Sends text events to the device through key codes.
+     * <p>
+     * Note: use this only when text accessibility is not supported.
+     * @param text the text to input as events
+     * @param delay the delay between each event
+     * @return true if successful, false otherwise
+     */
+    abstract boolean sendTextEvents(String text, long delay);
 }
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/exceptions/AccountException.java b/libraries/app-helpers/base/src/android/platform/test/helpers/base/exceptions/AccountException.java
similarity index 100%
rename from libraries/base-app-helpers/src/android/platform/test/helpers/exceptions/AccountException.java
rename to libraries/app-helpers/base/src/android/platform/test/helpers/base/exceptions/AccountException.java
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/exceptions/UiTimeoutException.java b/libraries/app-helpers/base/src/android/platform/test/helpers/base/exceptions/UiTimeoutException.java
similarity index 100%
rename from libraries/base-app-helpers/src/android/platform/test/helpers/exceptions/UiTimeoutException.java
rename to libraries/app-helpers/base/src/android/platform/test/helpers/base/exceptions/UiTimeoutException.java
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/exceptions/UnknownUiException.java b/libraries/app-helpers/base/src/android/platform/test/helpers/base/exceptions/UnknownUiException.java
similarity index 100%
rename from libraries/base-app-helpers/src/android/platform/test/helpers/exceptions/UnknownUiException.java
rename to libraries/app-helpers/base/src/android/platform/test/helpers/base/exceptions/UnknownUiException.java
diff --git a/libraries/app-helpers/base/src/android/platform/test/helpers/base/listeners/FailureTestWatcher.java b/libraries/app-helpers/base/src/android/platform/test/helpers/base/listeners/FailureTestWatcher.java
new file mode 100644
index 0000000..dde0812
--- /dev/null
+++ b/libraries/app-helpers/base/src/android/platform/test/helpers/base/listeners/FailureTestWatcher.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.helpers.listeners;
+
+import android.platform.test.helpers.IStandardAppHelper;
+import android.support.annotation.Nullable;
+import android.util.Log;
+
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
+
+import java.io.IOException;
+
+/**
+ * A JUnit {@code TestWatcher} for collecting screenshots and UI XML files on test failure.
+ */
+public class FailureTestWatcher extends TestWatcher {
+    private static final String LOG_TAG = FailureTestWatcher.class.getSimpleName();
+    private static final String SCREENSHOT_NAME_FORMAT = "%s_%s";
+
+    @Nullable
+    private IStandardAppHelper mHelper;
+
+    public void setHelper (IStandardAppHelper helper) {
+        mHelper = helper;
+    }
+
+    @Override
+    protected void failed(Throwable e, Description description) {
+        try {
+            if (!mHelper.captureScreenshot(String.format(SCREENSHOT_NAME_FORMAT,
+                    description.getClassName(), description.getMethodName()))) {
+                Log.e(LOG_TAG, "Failed to capture a screenshot for unknown reasons.");
+            }
+        } catch (IOException ioe) {
+            Log.e(LOG_TAG, "Failed to capture a screenshot.", ioe);
+        }
+    }
+}
diff --git a/libraries/base-app-helpers/Android.mk b/libraries/app-helpers/clockwork/Android.mk
similarity index 78%
copy from libraries/base-app-helpers/Android.mk
copy to libraries/app-helpers/clockwork/Android.mk
index 0111a0a..abb2d44 100644
--- a/libraries/base-app-helpers/Android.mk
+++ b/libraries/app-helpers/clockwork/Android.mk
@@ -1,5 +1,5 @@
 #
-# Copyright (C) 2016 The Android Open Source Project
+# Copyright (C) 2017 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -16,9 +16,8 @@
 LOCAL_PATH := $(call my-dir)
 
 include $(CLEAR_VARS)
-LOCAL_MODULE := base-app-helpers
-LOCAL_STATIC_JAVA_LIBRARIES := dpad-util
-LOCAL_JAVA_LIBRARIES := ub-uiautomator launcher-helper-lib
+LOCAL_MODULE := clockwork-app-helper-base
+LOCAL_STATIC_JAVA_LIBRARIES := app-helpers-common
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
 include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/app-helpers/clockwork/src/android/platform/test/helpers/clockwork/README b/libraries/app-helpers/clockwork/src/android/platform/test/helpers/clockwork/README
new file mode 100644
index 0000000..bb4f593
--- /dev/null
+++ b/libraries/app-helpers/clockwork/src/android/platform/test/helpers/clockwork/README
@@ -0,0 +1 @@
+A directory for wear-specific App Helpers.
diff --git a/libraries/base-app-helpers/Android.mk b/libraries/app-helpers/common/Android.mk
similarity index 72%
copy from libraries/base-app-helpers/Android.mk
copy to libraries/app-helpers/common/Android.mk
index 0111a0a..a3a370f 100644
--- a/libraries/base-app-helpers/Android.mk
+++ b/libraries/app-helpers/common/Android.mk
@@ -16,9 +16,13 @@
 LOCAL_PATH := $(call my-dir)
 
 include $(CLEAR_VARS)
-LOCAL_MODULE := base-app-helpers
-LOCAL_STATIC_JAVA_LIBRARIES := dpad-util
-LOCAL_JAVA_LIBRARIES := ub-uiautomator launcher-helper-lib
+LOCAL_MODULE := app-helpers-common
+LOCAL_STATIC_JAVA_LIBRARIES := app-helpers-base
+LOCAL_JAVA_LIBRARIES := ub-uiautomator android-support-test permission-utils-lib launcher-helper-lib
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
 include $(BUILD_STATIC_JAVA_LIBRARY)
+
+######################################
+
+include $(call all-makefiles-under, $(LOCAL_PATH))
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractGoogleFitHelper.java b/libraries/app-helpers/common/src/android/platform/test/helpers/common/AbstractGoogleFitHelper.java
similarity index 100%
rename from libraries/base-app-helpers/src/android/platform/test/helpers/AbstractGoogleFitHelper.java
rename to libraries/app-helpers/common/src/android/platform/test/helpers/common/AbstractGoogleFitHelper.java
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractGoogleKeyboardHelper.java b/libraries/app-helpers/common/src/android/platform/test/helpers/common/AbstractGoogleKeyboardHelper.java
similarity index 100%
rename from libraries/base-app-helpers/src/android/platform/test/helpers/AbstractGoogleKeyboardHelper.java
rename to libraries/app-helpers/common/src/android/platform/test/helpers/common/AbstractGoogleKeyboardHelper.java
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractPlayMoviesHelper.java b/libraries/app-helpers/common/src/android/platform/test/helpers/common/AbstractPlayMoviesHelper.java
similarity index 100%
rename from libraries/base-app-helpers/src/android/platform/test/helpers/AbstractPlayMoviesHelper.java
rename to libraries/app-helpers/common/src/android/platform/test/helpers/common/AbstractPlayMoviesHelper.java
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractPlayMusicHelper.java b/libraries/app-helpers/common/src/android/platform/test/helpers/common/AbstractPlayMusicHelper.java
similarity index 96%
rename from libraries/base-app-helpers/src/android/platform/test/helpers/AbstractPlayMusicHelper.java
rename to libraries/app-helpers/common/src/android/platform/test/helpers/common/AbstractPlayMusicHelper.java
index e2e8299..7002053 100644
--- a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractPlayMusicHelper.java
+++ b/libraries/app-helpers/common/src/android/platform/test/helpers/common/AbstractPlayMusicHelper.java
@@ -35,10 +35,10 @@
     /**
      * Setup expectations: PlayMusic is open and the navigation bar is visible.
      *
-     * This method will open the navigation bar, press "Listen Now".
+     * This method will open the navigation bar, press "Home".
      * This method blocks until the process is complete.
      */
-    public abstract void goToListenNow();
+    public abstract void goToHome();
 
 
     /**
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractPlayStoreHelper.java b/libraries/app-helpers/common/src/android/platform/test/helpers/common/AbstractPlayStoreHelper.java
similarity index 100%
rename from libraries/base-app-helpers/src/android/platform/test/helpers/AbstractPlayStoreHelper.java
rename to libraries/app-helpers/common/src/android/platform/test/helpers/common/AbstractPlayStoreHelper.java
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractStandardAppHelper.java b/libraries/app-helpers/common/src/android/platform/test/helpers/common/AbstractStandardAppHelper.java
similarity index 65%
rename from libraries/base-app-helpers/src/android/platform/test/helpers/AbstractStandardAppHelper.java
rename to libraries/app-helpers/common/src/android/platform/test/helpers/common/AbstractStandardAppHelper.java
index ea8e46a..097ea87 100644
--- a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractStandardAppHelper.java
+++ b/libraries/app-helpers/common/src/android/platform/test/helpers/common/AbstractStandardAppHelper.java
@@ -23,16 +23,32 @@
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Environment;
+import android.os.SystemClock;
 import android.platform.test.helpers.exceptions.AccountException;
 import android.support.test.launcherhelper.ILauncherStrategy;
 import android.support.test.launcherhelper.LauncherStrategyFactory;
 import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Configurator;
 import android.support.test.uiautomator.UiDevice;
+import android.util.Log;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+
+import java.io.File;
+import java.io.IOException;
 
 public abstract class AbstractStandardAppHelper implements IStandardAppHelper {
+    private static final String LOG_TAG = AbstractStandardAppHelper.class.getSimpleName();
+    private static final String SCREENSHOT_DIR = "apphelper-screenshots";
+
+    private static File sScreenshotDirectory;
+
     public UiDevice mDevice;
     public Instrumentation mInstrumentation;
     public ILauncherStrategy mLauncherStrategy;
+    private final KeyCharacterMap mKeyCharacterMap =
+            KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
 
     public AbstractStandardAppHelper(Instrumentation instr) {
         mInstrumentation = instr;
@@ -49,6 +65,7 @@
         String id = getLauncherName();
         if (!mDevice.hasObject(By.pkg(pkg).depth(0))) {
             mLauncherStrategy.launch(id, pkg);
+            Log.i(LOG_TAG, "Launched package: id=" + id + ", pkg=" + pkg);
         }
     }
 
@@ -123,4 +140,44 @@
         }
         return false;
     }
+
+    @Override
+    public boolean captureScreenshot(String name) throws IOException {
+        File scrOut = File.createTempFile(name, ".png", getScreenshotDirectory());
+        File uixOut = File.createTempFile(name, ".uix", getScreenshotDirectory());
+        mDevice.dumpWindowHierarchy(uixOut);
+        return mDevice.takeScreenshot(scrOut);
+    }
+
+    private static File getScreenshotDirectory() {
+        if (sScreenshotDirectory == null) {
+            File storage = Environment.getExternalStorageDirectory();
+            sScreenshotDirectory = new File(storage, SCREENSHOT_DIR);
+            if (!sScreenshotDirectory.exists()) {
+                if (!sScreenshotDirectory.mkdirs()) {
+                    throw new RuntimeException(
+                            "Failed to create a screenshot directory.");
+                }
+            }
+        }
+        return sScreenshotDirectory;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean sendTextEvents (String text, long delay) {
+        Log.v(LOG_TAG, String.format("Sending text events for %s", text));
+        KeyEvent[] events = mKeyCharacterMap.getEvents(text.toCharArray());
+        for (KeyEvent event : events) {
+            if (KeyEvent.ACTION_DOWN == event.getAction()) {
+                if (!mDevice.pressKeyCode(event.getKeyCode(), event.getMetaState())) {
+                    return false;
+                }
+                SystemClock.sleep(delay);
+            }
+        }
+        return true;
+    }
 }
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractYouTubeHelper.java b/libraries/app-helpers/common/src/android/platform/test/helpers/common/AbstractYouTubeHelper.java
similarity index 100%
rename from libraries/base-app-helpers/src/android/platform/test/helpers/AbstractYouTubeHelper.java
rename to libraries/app-helpers/common/src/android/platform/test/helpers/common/AbstractYouTubeHelper.java
diff --git a/libraries/app-helpers/common/src/android/platform/test/helpers/common/IMapsHelper.java b/libraries/app-helpers/common/src/android/platform/test/helpers/common/IMapsHelper.java
new file mode 100644
index 0000000..75d844d
--- /dev/null
+++ b/libraries/app-helpers/common/src/android/platform/test/helpers/common/IMapsHelper.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.helpers;
+
+import android.app.Instrumentation;
+
+public interface IMapsHelper extends IStandardAppHelper {
+    /**
+     * Setup expectation: On the standard Map screen in any setup.
+     *
+     * Best effort attempt to go to the query screen (if not currently there),
+     * does a search, and selects the results.
+     */
+    public abstract void doSearch(String query);
+
+    /**
+     * Setup expectation: Destination is selected.
+     *
+     * Best effort attempt to go to the directions screen for the selected destination.
+     */
+    public abstract void getDirections();
+
+    /**
+     * Setup expectation: On directions screen.
+     *
+     * Best effort attempt to start navigation for the selected destination.
+     */
+    public abstract void startNavigation();
+
+    /**
+     * Setup expectation: On navigation screen.
+     *
+     * Best effort attempt to stop navigation, and go back to the directions screen.
+     */
+    public abstract void stopNavigation();
+}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/IRecentsHelper.java b/libraries/app-helpers/common/src/android/platform/test/helpers/common/IRecentsHelper.java
similarity index 95%
rename from libraries/base-app-helpers/src/android/platform/test/helpers/IRecentsHelper.java
rename to libraries/app-helpers/common/src/android/platform/test/helpers/common/IRecentsHelper.java
index 8a6f054..e7cf763 100644
--- a/libraries/base-app-helpers/src/android/platform/test/helpers/IRecentsHelper.java
+++ b/libraries/app-helpers/common/src/android/platform/test/helpers/common/IRecentsHelper.java
@@ -18,7 +18,7 @@
 
 import android.support.test.uiautomator.Direction;
 
-public interface IRecentsHelper {
+public interface IRecentsHelper extends IStandardAppHelper {
     /**
      * Setup expectations: "Recents" is open.
      * <p>
diff --git a/libraries/app-helpers/common/src/android/platform/test/helpers/common/test/HelperTest.java b/libraries/app-helpers/common/src/android/platform/test/helpers/common/test/HelperTest.java
new file mode 100644
index 0000000..eddb30d
--- /dev/null
+++ b/libraries/app-helpers/common/src/android/platform/test/helpers/common/test/HelperTest.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.helpers.common.test;
+
+import android.os.Bundle;
+import android.platform.test.helpers.HelperManager;
+import android.platform.test.helpers.IStandardAppHelper;
+import android.platform.test.helpers.listeners.FailureTestWatcher;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.uiautomator.UiDevice;
+import android.util.Log;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.rules.Timeout;
+
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+
+import com.android.permissionutils.GrantPermissionUtil;
+
+/**
+ * A base-class for testing app helper implementations.
+ *
+ * @param T the helper interface under test.
+ */
+public abstract class HelperTest<T extends IStandardAppHelper> {
+    private static final String LOG_TAG = HelperTest.class.getSimpleName();
+    private static final String SKIP_INIT_PARAM = "skip-init";
+
+    // Keep track (across tests) of the initialized applications.
+    private static Map<Class, Boolean> mInitMap = new HashMap<Class, Boolean>();
+
+    // Global 5-minute test timeout
+    @Rule
+    public final TestRule timeout = Timeout.millis(Duration.ofMinutes(5).toMillis());
+
+    // Global screenshot capture on failures
+    public FailureTestWatcher watcher = new FailureTestWatcher();
+
+    @Rule
+    public FailureTestWatcher setUpWatcher() {
+        watcher.setHelper(getHelper());
+        return watcher;
+    }
+
+    protected UiDevice mDevice;
+    protected T mHelper;
+
+    /**
+     * Set up the target application before each test case starts.
+     */
+    @Before
+    public void setUp() {
+        // Initialize each application once on the first open unless skipped.
+        if (!mInitMap.containsKey(getHelperClass()) &&
+                !"true".equals(getArguments().get(SKIP_INIT_PARAM))) {
+            initialize();
+            mInitMap.put(getHelperClass(), true);
+        }
+        resetApp();
+        openApp();
+    }
+
+    /**
+     * Tear down the target application after each test case completes.
+     */
+    @After
+    public void tearDown() {
+        exitApp();
+    }
+
+    /**
+     * An empty test that ensures setup and initialization work properly.
+     */
+    @Test
+    public void testDismissDialogs() { }
+
+    /**
+     * Initialize the target application once before the test suite starts.
+     */
+    public void initialize() {
+        openApp();
+        getHelper().dismissInitialDialogs();
+        exitApp();
+    }
+
+    /**
+     * Reset the target application.
+     */
+    public void resetApp() { }
+
+    /**
+     * Open the target application.
+     */
+    public void openApp() {
+        // Open the application.
+        getHelper().open();
+    }
+
+    /**
+     * Exit the target application.
+     */
+    public void exitApp() {
+        // Exit the application.
+        getHelper().exit();
+    }
+
+    /**
+     * @return the connected {@link UiDevice}
+     */
+    public UiDevice getDevice() {
+        if (mDevice == null) {
+            mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        }
+
+        return mDevice;
+    }
+
+    /**
+     * @return the {@link Bundle} of arguments
+     */
+    public Bundle getArguments() {
+        return InstrumentationRegistry.getArguments();
+    }
+
+    /**
+     * @return an implementation for {@code T}
+     */
+    public T getHelper() {
+        if (mHelper == null) {
+            mHelper = HelperManager.getInstance(
+                    InstrumentationRegistry.getContext(),
+                    InstrumentationRegistry.getInstrumentation())
+                        .get(getHelperClass());
+        }
+
+        return mHelper;
+    }
+
+    protected abstract Class<T> getHelperClass();
+}
diff --git a/libraries/base-app-helpers/Android.mk b/libraries/app-helpers/handheld/Android.mk
similarity index 77%
copy from libraries/base-app-helpers/Android.mk
copy to libraries/app-helpers/handheld/Android.mk
index 0111a0a..623e0ee 100644
--- a/libraries/base-app-helpers/Android.mk
+++ b/libraries/app-helpers/handheld/Android.mk
@@ -1,5 +1,5 @@
 #
-# Copyright (C) 2016 The Android Open Source Project
+# Copyright (C) 2017 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -16,9 +16,9 @@
 LOCAL_PATH := $(call my-dir)
 
 include $(CLEAR_VARS)
-LOCAL_MODULE := base-app-helpers
-LOCAL_STATIC_JAVA_LIBRARIES := dpad-util
-LOCAL_JAVA_LIBRARIES := ub-uiautomator launcher-helper-lib
+LOCAL_MODULE := handheld-app-helper-base
+LOCAL_STATIC_JAVA_LIBRARIES := app-helpers-common
+LOCAL_JAVA_LIBRARIES := ub-uiautomator android-support-test
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
 include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/app-helpers/handheld/src/android/platform/test/helpers/handheld/AbstractDownloadsHelper.java b/libraries/app-helpers/handheld/src/android/platform/test/helpers/handheld/AbstractDownloadsHelper.java
new file mode 100644
index 0000000..a0ae37c
--- /dev/null
+++ b/libraries/app-helpers/handheld/src/android/platform/test/helpers/handheld/AbstractDownloadsHelper.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.helpers;
+
+import android.app.Instrumentation;
+
+public abstract class AbstractDownloadsHelper extends AbstractStandardAppHelper {
+
+    public static enum Category {
+        AUDIO,
+        IMAGES,
+        RECENT,
+        VIDEOS
+    }
+
+    public AbstractDownloadsHelper(Instrumentation instr) {
+        super(instr);
+    }
+
+    /**
+     * Setup expectation: Downloads app's Navigation Drawer is open
+     * <p>
+     * This method will select an item from the navigation drawer's list
+     *
+     * @param category - menu item to select (click)
+     */
+    public abstract void selectMenuCategory(Category category);
+
+    /**
+     * Setup expectation: Item has been selected from Navigation Drawer's list
+     * <p>
+     * This method opens a directory from the directories list
+     *
+     * @param directoryName - name of directory to open
+     */
+    public abstract void selectDirectory(String directoryName);
+
+    /**
+     * Setup expectation: Navigated to the right folder
+     * <p>
+     * This method clicks a specific file with name 'filename'
+     *
+     * @param filename - name of file to open
+     */
+    public abstract void openFile(String filename);
+
+    /**
+     * Setup expectation: Video is playing
+     * <p>
+     * This method will wait for the video to stop playing or until timeoutInSeconds occur,
+     * whichever comes first. Function will just exit, no test failure in either case.
+     *
+     * @param timeoutInSeconds - timeout value in seconds the test will wait for video to end
+     */
+    public abstract void waitForVideoToStopPlaying(long timeoutInSeconds);
+
+    /**
+     * Setup expectation: Audio is playing
+     * <p>
+     * This method will wait for the audio to stop playing or until timeoutInSeconds occur,
+     * whichever comes first. Function will just exit, no test failure in either case.
+     *
+     * @param timeoutInSeconds - timeout value in seconds the test will wait for audio to end
+     */
+    public abstract void waitForAudioToStopPlaying(long timeoutInSeconds);
+
+    /**
+     * Setup expectation: Video is playing
+     * <p>
+     * This method will enable or disable video looping. It will bring up the options menu and
+     * check the "Loop video" option
+     *
+     * @param enableVideoLooping - true for continuous looping video, false for not looping video
+     */
+    public abstract void enableVideoLooping(boolean enableVideoLooping);
+}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractGmailHelper.java b/libraries/app-helpers/handheld/src/android/platform/test/helpers/handheld/AbstractGmailHelper.java
similarity index 100%
rename from libraries/base-app-helpers/src/android/platform/test/helpers/AbstractGmailHelper.java
rename to libraries/app-helpers/handheld/src/android/platform/test/helpers/handheld/AbstractGmailHelper.java
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractGoogleCameraHelper.java b/libraries/app-helpers/handheld/src/android/platform/test/helpers/handheld/AbstractGoogleCameraHelper.java
similarity index 100%
rename from libraries/base-app-helpers/src/android/platform/test/helpers/AbstractGoogleCameraHelper.java
rename to libraries/app-helpers/handheld/src/android/platform/test/helpers/handheld/AbstractGoogleCameraHelper.java
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractGoogleDocsHelper.java b/libraries/app-helpers/handheld/src/android/platform/test/helpers/handheld/AbstractGoogleDocsHelper.java
similarity index 100%
rename from libraries/base-app-helpers/src/android/platform/test/helpers/AbstractGoogleDocsHelper.java
rename to libraries/app-helpers/handheld/src/android/platform/test/helpers/handheld/AbstractGoogleDocsHelper.java
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractGoogleMessengerHelper.java b/libraries/app-helpers/handheld/src/android/platform/test/helpers/handheld/AbstractGoogleMessengerHelper.java
similarity index 100%
rename from libraries/base-app-helpers/src/android/platform/test/helpers/AbstractGoogleMessengerHelper.java
rename to libraries/app-helpers/handheld/src/android/platform/test/helpers/handheld/AbstractGoogleMessengerHelper.java
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractMapsHelper.java b/libraries/app-helpers/handheld/src/android/platform/test/helpers/handheld/AbstractMapsHelper.java
similarity index 100%
rename from libraries/base-app-helpers/src/android/platform/test/helpers/AbstractMapsHelper.java
rename to libraries/app-helpers/handheld/src/android/platform/test/helpers/handheld/AbstractMapsHelper.java
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractPhotosHelper.java b/libraries/app-helpers/handheld/src/android/platform/test/helpers/handheld/AbstractPhotosHelper.java
similarity index 100%
rename from libraries/base-app-helpers/src/android/platform/test/helpers/AbstractPhotosHelper.java
rename to libraries/app-helpers/handheld/src/android/platform/test/helpers/handheld/AbstractPhotosHelper.java
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractPlayBooksHelper.java b/libraries/app-helpers/handheld/src/android/platform/test/helpers/handheld/AbstractPlayBooksHelper.java
similarity index 100%
rename from libraries/base-app-helpers/src/android/platform/test/helpers/AbstractPlayBooksHelper.java
rename to libraries/app-helpers/handheld/src/android/platform/test/helpers/handheld/AbstractPlayBooksHelper.java
diff --git a/tests/jank/notificationsgenerator_wear/src/com/google/android/wearable/support/CustomNotificationRemoteInputActivity.java b/libraries/app-helpers/handheld/src/android/platform/test/helpers/handheld/AbstractQuickSearchBoxHelper.java
similarity index 70%
copy from tests/jank/notificationsgenerator_wear/src/com/google/android/wearable/support/CustomNotificationRemoteInputActivity.java
copy to libraries/app-helpers/handheld/src/android/platform/test/helpers/handheld/AbstractQuickSearchBoxHelper.java
index b58f8df..ee693ea 100644
--- a/tests/jank/notificationsgenerator_wear/src/com/google/android/wearable/support/CustomNotificationRemoteInputActivity.java
+++ b/libraries/app-helpers/handheld/src/android/platform/test/helpers/handheld/AbstractQuickSearchBoxHelper.java
@@ -1,5 +1,4 @@
 /*
-
  * Copyright (C) 2016 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -15,10 +14,12 @@
  * limitations under the License.
  */
 
-package com.google.android.wearable.support;
+package android.platform.test.helpers;
 
-import android.app.Activity;
+import android.app.Instrumentation;
 
-public class CustomNotificationRemoteInputActivity extends Activity{
-
+public abstract class AbstractQuickSearchBoxHelper extends AbstractStandardAppHelper {
+    public AbstractQuickSearchBoxHelper(Instrumentation instr) {
+        super(instr);
+    }
 }
diff --git a/libraries/app-helpers/handheld/src/android/platform/test/helpers/handheld/AbstractSystemUpdateHelper.java b/libraries/app-helpers/handheld/src/android/platform/test/helpers/handheld/AbstractSystemUpdateHelper.java
new file mode 100644
index 0000000..8c157d2
--- /dev/null
+++ b/libraries/app-helpers/handheld/src/android/platform/test/helpers/handheld/AbstractSystemUpdateHelper.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.helpers;
+
+import android.app.Instrumentation;
+import android.content.Intent;
+
+public abstract class AbstractSystemUpdateHelper extends AbstractStandardAppHelper {
+
+    protected final static String SYSTEM_UPDATE = "android.settings.SYSTEM_UPDATE_SETTINGS";
+    public AbstractSystemUpdateHelper(Instrumentation instr) {
+        super(instr);
+    }
+
+    /**
+     * As System Update is a subcomponent of Settings, it will not appear in the launcher.
+     * It needs to be opened directly via its activity.
+     */
+    @Override
+    public void open() {
+        Intent startIntent = new Intent(SYSTEM_UPDATE);
+        startIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        mInstrumentation.getContext().startActivity(
+                startIntent);
+        try {
+            // wait for app to open
+            Thread.sleep(7000);
+        } catch (InterruptedException ignored) {
+            // do nothing
+        }
+    }
+
+    /**
+     * Check whether or not an update is available
+     * @return true if the device has an update available, false otherwise.
+     */
+    public abstract boolean isUpdateAvailable();
+
+    /**
+     * If an update is available, download it. Otherwise, throw {@link IllegalStateException}.
+     * Precondition: The device is on the System Update screen.
+     * Postcondition: A system update will be ready to install.
+     * @return true if the download succeeded
+     */
+    public abstract boolean downloadUpdate();
+
+    /**
+     * Click on an existing OTA notification.
+     * Precondition: The notification drawer is open and an OTA notification exists.
+     * Postcondition: The device is on the System Update screen.
+     */
+    public abstract void clickOtaNotification();
+
+    /**
+     * Check whether or not an OTA notification is present
+     * @return true if an OTA notification is in the notification drawer, false otherwise.
+     */
+    public abstract boolean hasOtaNotification();
+
+    /**
+     * Check whether or not an attempted OTA download is completed
+     * @return true if an OTA is ready to install, false otherwise
+     */
+    public abstract boolean isOtaDownloadCompleted();
+
+    /**
+     * Install an OTA. This will cause the device to power off.
+     * Precondition: A system update is ready to install.
+     * Postcondition: The device will reboot.
+     * @return true if the "Install" button was successfully clicked, false otherwise
+     */
+    public abstract boolean installOta();
+}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractChromeHelper.java b/libraries/app-helpers/handheld/src/android/platform/test/helpers/handheld/IChromeHelper.java
similarity index 92%
rename from libraries/base-app-helpers/src/android/platform/test/helpers/AbstractChromeHelper.java
rename to libraries/app-helpers/handheld/src/android/platform/test/helpers/handheld/IChromeHelper.java
index 7589d22..9362fec 100644
--- a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractChromeHelper.java
+++ b/libraries/app-helpers/handheld/src/android/platform/test/helpers/handheld/IChromeHelper.java
@@ -19,12 +19,7 @@
 import android.app.Instrumentation;
 import android.support.test.uiautomator.Direction;
 
-public abstract class AbstractChromeHelper extends AbstractStandardAppHelper {
-
-    public AbstractChromeHelper(Instrumentation instr) {
-        super(instr);
-    }
-
+public interface IChromeHelper extends IStandardAppHelper {
     /**
      * Setup expectations: Chrome is open and on a standard page, i.e. a tab is open.
      *
diff --git a/libraries/app-helpers/handheld/src/android/platform/test/helpers/handheld/test/HandheldHelperTest.java b/libraries/app-helpers/handheld/src/android/platform/test/helpers/handheld/test/HandheldHelperTest.java
new file mode 100644
index 0000000..03703f4
--- /dev/null
+++ b/libraries/app-helpers/handheld/src/android/platform/test/helpers/handheld/test/HandheldHelperTest.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.helpers.tests;
+
+import android.os.RemoteException;
+import android.platform.test.helpers.common.test.HelperTest;
+import android.platform.test.helpers.IStandardAppHelper;
+
+public abstract class HandheldHelperTest<T extends IStandardAppHelper> extends HelperTest<T> {
+    @Override
+    public void initialize() {
+        try {
+            getDevice().setOrientationNatural();
+        } catch (RemoteException e) {
+            throw new RuntimeException("Could not set orientation.", e);
+        }
+        super.initialize();
+    }
+}
diff --git a/libraries/base-app-helpers/Android.mk b/libraries/app-helpers/tv/Android.mk
similarity index 83%
copy from libraries/base-app-helpers/Android.mk
copy to libraries/app-helpers/tv/Android.mk
index 0111a0a..4d05a2b 100644
--- a/libraries/base-app-helpers/Android.mk
+++ b/libraries/app-helpers/tv/Android.mk
@@ -1,5 +1,5 @@
 #
-# Copyright (C) 2016 The Android Open Source Project
+# Copyright (C) 2017 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -16,8 +16,8 @@
 LOCAL_PATH := $(call my-dir)
 
 include $(CLEAR_VARS)
-LOCAL_MODULE := base-app-helpers
-LOCAL_STATIC_JAVA_LIBRARIES := dpad-util
+LOCAL_MODULE := tv-app-helper-base
+LOCAL_STATIC_JAVA_LIBRARIES := app-helpers-common dpad-util
 LOCAL_JAVA_LIBRARIES := ub-uiautomator launcher-helper-lib
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractLeanbackAppHelper.java b/libraries/app-helpers/tv/src/android/platform/test/helpers/tv/AbstractLeanbackAppHelper.java
similarity index 68%
rename from libraries/base-app-helpers/src/android/platform/test/helpers/AbstractLeanbackAppHelper.java
rename to libraries/app-helpers/tv/src/android/platform/test/helpers/tv/AbstractLeanbackAppHelper.java
index f8ea94f..b28277f 100644
--- a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractLeanbackAppHelper.java
+++ b/libraries/app-helpers/tv/src/android/platform/test/helpers/tv/AbstractLeanbackAppHelper.java
@@ -17,6 +17,7 @@
 package android.platform.test.helpers;
 
 import android.app.Instrumentation;
+import android.os.SystemClock;
 import android.platform.test.helpers.exceptions.UiTimeoutException;
 import android.platform.test.helpers.exceptions.UnknownUiException;
 import android.platform.test.utils.DPadUtil;
@@ -40,7 +41,8 @@
     private static final long OPEN_HEADER_WAIT_TIME_MS = 5000;
     private static final int OPEN_SIDE_PANEL_MAX_ATTEMPTS = 5;
     private static final long MAIN_ACTIVITY_WAIT_TIME_MS = 250;
-    private static final long SELECT_WAIT_TIME_MS = 5000;
+    private static final long UI_WAIT_TIME_MS = 5000;
+    private static final long FOCUS_WAIT_TIME_MS = 100;
 
     // The notable widget classes in Leanback Library
     public enum Widget {
@@ -57,12 +59,20 @@
     protected DPadUtil mDPadUtil;
     public ILeanbackLauncherStrategy mLauncherStrategy;
 
+    /**
+     * A condition to be satisfied by BaseView or UI actions. The condition can use a given
+     * focused {@link UiObject2}.
+     */
+    public abstract class SelectCondition {
+        public abstract boolean apply(UiObject2 focus);
+    }
 
     public AbstractLeanbackAppHelper(Instrumentation instr) {
         super(instr);
         mDPadUtil = new DPadUtil(instr);
         mLauncherStrategy = LauncherStrategyFactory.getInstance(
                 mDevice).getLeanbackLauncherStrategy();
+        mLauncherStrategy.setInstrumentation(instr);
     }
 
     /**
@@ -149,6 +159,7 @@
      * @param timeoutMs
      */
     public void open(long timeoutMs) {
+        Log.v(TAG, String.format("[%s] Opening the package in %d(ms)", getPackage(), timeoutMs));
         open();
         if (!waitForOpen(timeoutMs)) {
             throw new UiTimeoutException(String.format("Timed out to open a target package %s:"
@@ -163,6 +174,7 @@
      * </p>
      */
     public void openHeader(String headerName) {
+        Log.v(TAG, String.format("[%s] Opening the header %s", getPackage(), headerName));
         openBrowseHeaders();
         // header is focused; it should not be after pressing the DPad
         selectHeader(headerName);
@@ -203,37 +215,94 @@
     }
 
     /**
-     * Select target item through the container in the given direction.
-     * @param container
-     * @param target
-     * @param direction
-     * @return the focused object
+     * Select an UI element with given {@link BySelector} traversing down from {@link UiObject2}
+     * This action keeps moving a focus in a given {@link Direction} until it finds matched element.
+     * @param condition the search criteria to match an element
+     * @param container the container {@link UiObject2} in which it searches for a focus.
+     *                  Use this when it needs to search for a certain area only.
+     *                  Set null if move a focus until it can't move any more.
+     * @param direction the direction to find
+     * @return a {@link UiObject2} which represents the matched element
      */
-    public UiObject2 select(UiObject2 container, BySelector target, Direction direction) {
-        if (container == null) {
-            throw new IllegalArgumentException("The container should not be null.");
-        }
-        UiObject2 focus = container.findObject(By.focused(true));
-        if (focus == null) {
-            throw new UnknownUiException("The container should have a focused descendant.");
-        }
-        while (!focus.hasObject(target)) {
-            UiObject2 prev = focus;
+    public UiObject2 select(SelectCondition condition, UiObject2 container, Direction direction) {
+        UiObject2 focus = findFocus(container, FOCUS_WAIT_TIME_MS);
+        while (!condition.apply(focus)) {
+            Log.d(TAG, String.format("select: moving a focus to %s from %s",
+                    direction, focus.toString()));
+            UiObject2 focused = focus;
             mDPadUtil.pressDPad(direction);
-            focus = container.findObject(By.focused(true));
-            if (focus == null) {
-                mDPadUtil.pressDPad(Direction.reverse(direction));
-                focus = container.findObject(By.focused(true));
-            }
-            if (focus.equals(prev)) {
-                // It reached at the end, but no target is found.
+            focus = findFocus(container, FOCUS_WAIT_TIME_MS);
+            // Check if it reaches to an end where it no longer moves a focus to next element
+            if (focused.equals(focus)) {
+                Log.d(TAG, "select: not found until it reaches to an end.");
                 return null;
             }
         }
+        Log.i(TAG, String.format("select: selected %s", focus.toString()));
         return focus;
     }
 
     /**
+     * Select target item through the container in the given direction.
+     * @param container
+     * @param selector
+     * @param direction
+     * @return the focused object
+     */
+    public UiObject2 select(UiObject2 container, final BySelector selector, Direction direction) {
+        return select(new SelectCondition() {
+            @Override
+            public boolean apply(UiObject2 focus) {
+                return focus.hasObject(selector);
+            }
+        }, container, direction);
+    }
+
+    public UiObject2 selectBidirect(SelectCondition condition, UiObject2 container,
+            Direction direction) {
+        UiObject2 object = select(condition, container, direction);
+        if (object == null) {
+            object = select(condition, container, Direction.reverse(direction));
+        }
+        return object;
+    }
+
+    public UiObject2 selectBidirect(final BySelector selector, UiObject2 container,
+            Direction direction) {
+        return selectBidirect(new SelectCondition() {
+            @Override
+            public boolean apply(UiObject2 focus) {
+                return focus.hasObject(selector);
+            }
+        }, container, direction);
+    }
+
+    /**
+     * Wait for an UI element to have a focus.
+     * @param fromObject {@link UiObject2} under which it searches for an element that is focused.
+     *               Set Null if it searches through the entire hierarchy of accessibility nodes
+     * @param timeoutMs Maximum amount of time to wait in milliseconds.
+     * @return The {@link UiObject2} that has a focused element, or null if no focus.
+     */
+    public UiObject2 findFocus(UiObject2 fromObject, long timeoutMs) {
+        UiObject2 focused;
+        if (fromObject == null) {
+            focused = mDevice.wait(Until.findObject(By.focused(true)), timeoutMs);
+        } else {
+            focused = fromObject.wait(Until.findObject(By.focused(true)), timeoutMs);
+        }
+        return focused;
+    }
+
+    /**
+     * Return the {@link UiObject2} that has a focused element searching through the entire view
+     * hierarchy, and throws an exception if no focus exists.
+     */
+    public UiObject2 findFocus() {
+        return findFocus(null, FOCUS_WAIT_TIME_MS);
+    }
+
+    /**
      * Setup expectation: On guided fragment.
      * <p>
      * Best effort attempt to select a given guided action.
@@ -244,7 +313,7 @@
         UiObject2 container = mDevice.wait(
                 Until.findObject(
                         By.res(getPackage(), "guidedactions_list").hasChild(By.focused(true))),
-                SELECT_WAIT_TIME_MS);
+                UI_WAIT_TIME_MS);
         // Search down, then up
         BySelector selector = By.res(getPackage(), "guidedactions_item_title").text(action);
         UiObject2 focused = select(container, selector, Direction.DOWN);
@@ -264,11 +333,36 @@
     public String getGuidanceTitleText() {
         assertWidgetEquals(Widget.GUIDED_STEP_FRAGMENT);
         UiObject2 object = mDevice.wait(
-                Until.findObject(By.res(getPackage(), "guidance_title")), SELECT_WAIT_TIME_MS);
+                Until.findObject(By.res(getPackage(), "guidance_title")), UI_WAIT_TIME_MS);
         return object.getText();
     }
 
     /**
+     * Setup expectation: On details fragment.
+     * <p>
+     * Best effort attempt to select a given details overview action.
+     * </p>
+     */
+    public UiObject2 selectDetailsOverviewAction(String action) {
+        assertWidgetEquals(Widget.DETAILS_FRAGMENT);
+
+        // Move a focus to the row where the action buttons are placed.
+        UiObject2 focused = selectBidirect(new SelectCondition() {
+            @Override
+            public boolean apply(UiObject2 focus) {
+                return mDevice.hasObject(By.res(getPackage(), "details_overview_actions").hasChild(
+                        By.focused(true)));
+            }
+        }, null, Direction.DOWN);
+        if (focused == null) {
+            throw new UnknownUiException("Failed to find the details_overview_actions row");
+        }
+
+        // Move a focus to the row where the action buttons are placed.
+        return selectBidirect(By.text(action), null, Direction.RIGHT);
+    }
+
+    /**
      * Setup expectation: On row fragment.
      * @param title of the card
      * @return UIObject2 for the focusable card that matches a given name in title
@@ -277,7 +371,7 @@
         assertWidgetEquals(Widget.BROWSE_ROWS_FRAGMENT);
         return mDevice.wait(Until.findObject(
                 By.focused(true).hasDescendant(By.res(getPackage(), "title_text").text(title))),
-                SELECT_WAIT_TIME_MS);
+                UI_WAIT_TIME_MS);
     }
 
     /**
@@ -324,7 +418,7 @@
         }
         mDPadUtil.pressDPadCenter();
         mDevice.wait(Until.gone(By.res(getPackage(), "title_text").text(title)),
-                SELECT_WAIT_TIME_MS);
+                UI_WAIT_TIME_MS);
     }
 
     /**
@@ -388,24 +482,38 @@
         return mDevice.wait(Until.hasObject(getBrowseHeadersSelector()), timeoutMs);
     }
 
+    /**
+     * Attempts to select a given header text three times with the backoff timeout each retry.
+     * The timeout needs to be long enough if it runs under low bandwidth environments.
+     */
     protected UiObject2 selectHeader(String headerName) {
+        long retryWaitMs = 10 * 1000;    // 10 sec
+        int maxAttempts = 3;
+        UiObject2 header;
+        while (maxAttempts-- > 0) {
+            header = selectHeader(headerName, Direction.DOWN);
+            if (header != null) {
+                return header;
+            }
+            retryWaitMs *= 2;
+            SystemClock.sleep(retryWaitMs);
+        }
+        throw new UnknownUiException("Failed to select header : " + headerName);
+    }
+
+    /**
+     * Moves a focus in a given direction and reverse direction to select the header
+     */
+    protected UiObject2 selectHeader(String headerName, Direction direction) {
+        Log.v(TAG, String.format("[%s] Selecting the header %s", getPackage(), headerName));
         UiObject2 container = mDevice.wait(
                 Until.findObject(getBrowseHeadersSelector()), OPEN_HEADER_WAIT_TIME_MS);
+        if (container == null) {
+            throw new UnknownUiException(String.format(
+                    "Failed to find the header [%s] in the browse header", headerName));
+        }
+
         BySelector header = By.clazz(".TextView").text(headerName);
-
-        // Wait until the row header text appears at runtime. This needs to be long enough to run
-        // under low bandwidth environments in the test lab.
-        mDevice.wait(Until.findObject(header), 60 * 1000);
-
-        // Search up, then down
-        UiObject2 focused = select(container, header, Direction.UP);
-        if (focused != null) {
-            return focused;
-        }
-        focused = select(container, header, Direction.DOWN);
-        if (focused != null) {
-            return focused;
-        }
-        throw new UnknownUiException("Failed to select header");
+        return selectBidirect(header, container, direction);
     }
 }
diff --git a/libraries/aupt-lib/Android.mk b/libraries/aupt-lib/Android.mk
index 7afb41e..ae6c2f2 100644
--- a/libraries/aupt-lib/Android.mk
+++ b/libraries/aupt-lib/Android.mk
@@ -18,8 +18,10 @@
 include $(CLEAR_VARS)
 
 LOCAL_MODULE := AuptLib
-LOCAL_SDK_VERSION := 21
-LOCAL_JAVA_LIBRARIES := ub-uiautomator
+LOCAL_JAVA_LIBRARIES := ub-uiautomator \
+    junit \
+    legacy-android-test \
+    android.test.runner
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
 include $(BUILD_STATIC_JAVA_LIBRARY)
@@ -30,8 +32,10 @@
 
 LOCAL_MODULE_TAGS := tests
 LOCAL_PACKAGE_NAME := AuptRunner
-LOCAL_SDK_VERSION := 21
-LOCAL_STATIC_JAVA_LIBRARIES := ub-uiautomator
+LOCAL_STATIC_JAVA_LIBRARIES := ub-uiautomator \
+    junit \
+    legacy-android-test \
+    android.test.runner
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
 include $(BUILD_PACKAGE)
diff --git a/libraries/aupt-lib/AndroidManifest.xml b/libraries/aupt-lib/AndroidManifest.xml
index 8e0f65c..f038bb4 100644
--- a/libraries/aupt-lib/AndroidManifest.xml
+++ b/libraries/aupt-lib/AndroidManifest.xml
@@ -15,10 +15,10 @@
 -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="android.support.test.aupt">
-    <uses-sdk android:minSdkVersion="2" android:targetSdkVersion="21" />
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.WAKE_LOCK" />
+    <uses-permission android:name="android.permission.DUMP" />
     <application>
         <uses-library android:name="android.test.runner"/>
     </application>
diff --git a/libraries/aupt-lib/src/android/support/test/aupt/AuptTestCase.java b/libraries/aupt-lib/src/android/support/test/aupt/AuptTestCase.java
index 7755fc7..0b6daab 100644
--- a/libraries/aupt-lib/src/android/support/test/aupt/AuptTestCase.java
+++ b/libraries/aupt-lib/src/android/support/test/aupt/AuptTestCase.java
@@ -30,6 +30,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.regex.Matcher;
@@ -37,13 +38,11 @@
 
 import android.accounts.Account;
 import android.accounts.AccountManager;
-import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.os.Bundle;
-import android.os.Environment;
 import android.os.SystemClock;
 import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.UiDevice;
@@ -58,346 +57,22 @@
  * Base class for AuptTests.
  */
 public class AuptTestCase extends InstrumentationTestCase {
-    private static final String SDCARD =
-            Environment.getExternalStorageDirectory().getAbsolutePath();
-    private static final String RECORD_MEMINFO_PARAM = "record_meminfo";
-    private static final long DEFAULT_SHORT_SLEEP = 5 * 1000;
-    private static final long DEFAULT_LONG_SLEEP = 30 * 1000;
-    protected static final int STEPS_BACK = 10;
-    protected static final int RECOVERY_SLEEP = 2000;
-    private static final String TAG = AuptTestCase.class.getSimpleName();
+    /* Constants */
+    static final int STEPS_BACK = 10;
+    static final int RECOVERY_SLEEP = 2000;
+    static final long DEFAULT_SHORT_SLEEP = 5 * 1000;
+    static final long DEFAULT_LONG_SLEEP = 30 * 1000;
+    static final String TAG = AuptTestCase.class.getSimpleName();
 
-    private boolean mRecordMeminfo = false;
+    /* State */
     private UiWatchers mWatchers;
-    private IProcessStatusTracker mProcessStatusTracker;
-    private DataCollector mDataCollector;
-    private List<String> mPaths;
-
-    // We want to periodically collect dumpheap output if the process grows to large to proactivelly
-    // help with catching memory leaks, but don't want to do it too often so it does not disturb the
-    // test. We are going to limit the total number of dumpheap commands per proccess to 5 by
-    // default, rate limit it to one per hour be default, and only do it for monitored processes
-    // which grow larger than a certain size (200MB by default).
-    private boolean mDumpheapEnabled = false;
-    private long mDumpheapThreshold;
-    private long mDumpheapInterval ;
-    private long mMaxDumpheaps;
-    private Map<String, Long> mLastDumpheap = new HashMap<String, Long>();
-    private Map<String, Long> mDumpheapCount = new HashMap<String, Long>();
-
     private UiDevice mDevice;
 
-    public static class MemHealthRecord {
-        public enum Context { FOREGROUND, BACKGROUND };
+    /* *******************  InstrumentationTestCase Hooks ******************* */
 
-        private final long mTimeMs;
-        private final long mDalvikHeap;
-        private final long mNativeHeap;
-        private final long mPss;
-
-        // App summary metrics
-        private final long mAsJavaHeap; // Private java heap
-        private final long mAsNativeHeap; // Private native heap
-        private final long mAsCode; // Private code
-        private final long mAsStack; // Private stack
-        private final long mAsGraphics; // Private graphics
-        private final long mAsOther; // Private other
-        private final long mAsSystem; // System
-        private final long mAsOverallPss; // Overall PSS
-
-        private final Context mContext;
-
-        public MemHealthRecord(long timeMs, long dalvikHeap, long nativeHeap, long pss,
-                long asJavaHeap, long asNativeHeap, long asCode, long asStack,
-                long asGraphics, long asOther, long asSystem, long asOverallPss,
-                Context context) {
-            mTimeMs = timeMs;
-            mDalvikHeap = dalvikHeap;
-            mNativeHeap = nativeHeap;
-            mPss = pss;
-            mAsJavaHeap = asJavaHeap;
-            mAsNativeHeap = asNativeHeap;
-            mAsCode = asCode;
-            mAsStack = asStack;
-            mAsGraphics = asGraphics;
-            mAsOther = asOther;
-            mAsSystem = asSystem;
-            mAsOverallPss = asOverallPss;
-            mContext = context;
-        }
-
-        public MemHealthRecord(long timeMs, long dalvikHeap, long nativeHeap, long pss,
-                Context context) {
-            mTimeMs = timeMs;
-            mDalvikHeap = dalvikHeap;
-            mNativeHeap = nativeHeap;
-            mPss = pss;
-            mAsJavaHeap = 0;
-            mAsNativeHeap = 0;
-            mAsCode = 0;
-            mAsStack = 0;
-            mAsGraphics = 0;
-            mAsOther = 0;
-            mAsSystem = 0;
-            mAsOverallPss = 0;
-            mContext = context;
-        }
-
-        public static List<Long> getForegroundDalvikHeap(Collection<MemHealthRecord> samples) {
-            List<Long> ret = new ArrayList<>(samples.size());
-            for (MemHealthRecord sample : samples) {
-                if (Context.FOREGROUND.equals(sample.mContext)) {
-                    ret.add(sample.mDalvikHeap);
-                }
-            }
-            return ret;
-        }
-
-        public static List<Long> getBackgroundDalvikHeap(Collection<MemHealthRecord> samples) {
-            List<Long> ret = new ArrayList<>(samples.size());
-            for (MemHealthRecord sample : samples) {
-                if (Context.BACKGROUND.equals(sample.mContext)) {
-                    ret.add(sample.mDalvikHeap);
-                }
-            }
-            return ret;
-        }
-
-        public static List<Long> getForegroundNativeHeap(Collection<MemHealthRecord> samples) {
-            List<Long> ret = new ArrayList<>(samples.size());
-            for (MemHealthRecord sample : samples) {
-                if (Context.FOREGROUND.equals(sample.mContext)) {
-                    ret.add(sample.mNativeHeap);
-                }
-            }
-            return ret;
-        }
-
-        public static List<Long> getBackgroundNativeHeap(Collection<MemHealthRecord> samples) {
-            List<Long> ret = new ArrayList<>(samples.size());
-            for (MemHealthRecord sample : samples) {
-                if (Context.BACKGROUND.equals(sample.mContext)) {
-                    ret.add(sample.mNativeHeap);
-                }
-            }
-            return ret;
-        }
-
-        public static List<Long> getForegroundPss(Collection<MemHealthRecord> samples) {
-            List<Long> ret = new ArrayList<>(samples.size());
-            for (MemHealthRecord sample : samples) {
-                if (Context.FOREGROUND.equals(sample.mContext)) {
-                    ret.add(sample.mPss);
-                }
-            }
-            return ret;
-        }
-
-        public static List<Long> getBackgroundPss(Collection<MemHealthRecord> samples) {
-            List<Long> ret = new ArrayList<>(samples.size());
-            for (MemHealthRecord sample : samples) {
-                if (Context.BACKGROUND.equals(sample.mContext)) {
-                    ret.add(sample.mPss);
-                }
-            }
-            return ret;
-        }
-
-        public static List<Long> getForegroundSummaryJavaHeap(Collection<MemHealthRecord> samples) {
-            List<Long> ret = new ArrayList<>(samples.size());
-            for (MemHealthRecord sample : samples) {
-                if (Context.FOREGROUND.equals(sample.mContext)) {
-                    ret.add(sample.mAsJavaHeap);
-                }
-            }
-            return ret;
-        }
-
-        public static List<Long> getBackgroundSummaryJavaHeap(Collection<MemHealthRecord> samples) {
-            List<Long> ret = new ArrayList<>(samples.size());
-            for (MemHealthRecord sample : samples) {
-                if (Context.BACKGROUND.equals(sample.mContext)) {
-                    ret.add(sample.mAsJavaHeap);
-                }
-            }
-            return ret;
-        }
-
-        public static List<Long> getForegroundSummaryNativeHeap(
-                    Collection<MemHealthRecord> samples) {
-            List<Long> ret = new ArrayList<>(samples.size());
-            for (MemHealthRecord sample : samples) {
-                if (Context.FOREGROUND.equals(sample.mContext)) {
-                    ret.add(sample.mAsNativeHeap);
-                }
-            }
-            return ret;
-        }
-
-        public static List<Long> getBackgroundSummaryNativeHeap(
-                    Collection<MemHealthRecord> samples) {
-            List<Long> ret = new ArrayList<>(samples.size());
-            for (MemHealthRecord sample : samples) {
-                if (Context.BACKGROUND.equals(sample.mContext)) {
-                    ret.add(sample.mAsNativeHeap);
-                }
-            }
-            return ret;
-        }
-
-        public static List<Long> getForegroundSummaryCode(Collection<MemHealthRecord> samples) {
-            List<Long> ret = new ArrayList<>(samples.size());
-            for (MemHealthRecord sample : samples) {
-                if (Context.FOREGROUND.equals(sample.mContext)) {
-                    ret.add(sample.mAsCode);
-                }
-            }
-            return ret;
-        }
-
-        public static List<Long> getBackgroundSummaryCode(Collection<MemHealthRecord> samples) {
-            List<Long> ret = new ArrayList<>(samples.size());
-            for (MemHealthRecord sample : samples) {
-                if (Context.BACKGROUND.equals(sample.mContext)) {
-                    ret.add(sample.mAsCode);
-                }
-            }
-            return ret;
-        }
-
-        public static List<Long> getForegroundSummaryStack(Collection<MemHealthRecord> samples) {
-            List<Long> ret = new ArrayList<>(samples.size());
-            for (MemHealthRecord sample : samples) {
-                if (Context.FOREGROUND.equals(sample.mContext)) {
-                    ret.add(sample.mAsStack);
-                }
-            }
-            return ret;
-        }
-
-        public static List<Long> getBackgroundSummaryStack(Collection<MemHealthRecord> samples) {
-            List<Long> ret = new ArrayList<>(samples.size());
-            for (MemHealthRecord sample : samples) {
-                if (Context.BACKGROUND.equals(sample.mContext)) {
-                    ret.add(sample.mAsStack);
-                }
-            }
-            return ret;
-        }
-
-        public static List<Long> getForegroundSummaryGraphics(Collection<MemHealthRecord> samples) {
-            List<Long> ret = new ArrayList<>(samples.size());
-            for (MemHealthRecord sample : samples) {
-                if (Context.FOREGROUND.equals(sample.mContext)) {
-                    ret.add(sample.mAsGraphics);
-                }
-            }
-            return ret;
-        }
-
-        public static List<Long> getBackgroundSummaryGraphics(Collection<MemHealthRecord> samples) {
-            List<Long> ret = new ArrayList<>(samples.size());
-            for (MemHealthRecord sample : samples) {
-                if (Context.BACKGROUND.equals(sample.mContext)) {
-                    ret.add(sample.mAsGraphics);
-                }
-            }
-            return ret;
-        }
-
-        public static List<Long> getForegroundSummaryOther(Collection<MemHealthRecord> samples) {
-            List<Long> ret = new ArrayList<>(samples.size());
-            for (MemHealthRecord sample : samples) {
-                if (Context.FOREGROUND.equals(sample.mContext)) {
-                    ret.add(sample.mAsOther);
-                }
-            }
-            return ret;
-        }
-
-        public static List<Long> getBackgroundSummaryOther(Collection<MemHealthRecord> samples) {
-            List<Long> ret = new ArrayList<>(samples.size());
-            for (MemHealthRecord sample : samples) {
-                if (Context.BACKGROUND.equals(sample.mContext)) {
-                    ret.add(sample.mAsOther);
-                }
-            }
-            return ret;
-        }
-
-        public static List<Long> getForegroundSummarySystem(Collection<MemHealthRecord> samples) {
-            List<Long> ret = new ArrayList<>(samples.size());
-            for (MemHealthRecord sample : samples) {
-                if (Context.FOREGROUND.equals(sample.mContext)) {
-                    ret.add(sample.mAsSystem);
-                }
-            }
-            return ret;
-        }
-
-        public static List<Long> getBackgroundSummarySystem(Collection<MemHealthRecord> samples) {
-            List<Long> ret = new ArrayList<>(samples.size());
-            for (MemHealthRecord sample : samples) {
-                if (Context.BACKGROUND.equals(sample.mContext)) {
-                    ret.add(sample.mAsSystem);
-                }
-            }
-            return ret;
-        }
-
-        public static List<Long> getForegroundSummaryOverallPss(
-                    Collection<MemHealthRecord> samples) {
-            List<Long> ret = new ArrayList<>(samples.size());
-            for (MemHealthRecord sample : samples) {
-                if (Context.FOREGROUND.equals(sample.mContext)) {
-                    ret.add(sample.mAsOverallPss);
-                }
-            }
-            return ret;
-        }
-
-        public static List<Long> getBackgroundSummaryOverallPss(
-                    Collection<MemHealthRecord> samples) {
-            List<Long> ret = new ArrayList<>(samples.size());
-            for (MemHealthRecord sample : samples) {
-                if (Context.BACKGROUND.equals(sample.mContext)) {
-                    ret.add(sample.mAsOverallPss);
-                }
-            }
-            return ret;
-        }
-
-        private static Long getMax(Collection<Long> samples) {
-            Long max = null;
-            for (Long sample : samples) {
-                if (max == null || sample > max) {
-                    max = sample;
-                }
-            }
-            return max;
-        }
-
-        private static Long getAverage(Collection<Long> samples) {
-            if (samples.size() == 0) {
-                return null;
-            }
-
-            double sum = 0;
-            for (Long sample : samples) {
-                sum += sample;
-            }
-            return (long) (sum / samples.size());
-        }
-    }
-
-    private Map<String, List<MemHealthRecord>> mMemHealthRecords;
-    private String[] mProcsToTrack;
-    private String mResultsDirectory;
-
-    public void setMemHealthRecords(Map<String, List<MemHealthRecord>> records) {
-        mMemHealthRecords = records;
-    }
-
+    /**
+     * {@inheritDoc}
+     */
     @Override
     protected void setUp() throws Exception {
         super.setUp();
@@ -405,47 +80,9 @@
         mDevice = UiDevice.getInstance(getInstrumentation());
         mWatchers = new UiWatchers();
         mWatchers.registerAnrAndCrashWatchers(getInstrumentation());
+
         mDevice.registerWatcher("LockScreenWatcher", new LockScreenWatcher());
-        mRecordMeminfo = "true".equals(getParams().getString(RECORD_MEMINFO_PARAM, "false"));
-
         mDevice.setOrientationNatural();
-
-        mResultsDirectory = SDCARD + "/" + getParams().getString(
-                "outputLocation", "aupt_results");
-
-        String processes = getParams().getString("trackMemory", null);
-        if (processes != null) {
-            mProcsToTrack = processes.split(",");
-        } else {
-            readProcessesFromFile();
-        }
-
-        mDumpheapEnabled = "true".equals(getParams().getString("enableDumpheap"));
-        if (mDumpheapEnabled) {
-            mDumpheapThreshold = getLongParam("dumpheapThreshold", 200 * 1024 * 1024); // 200MB
-            mDumpheapInterval = getLongParam("dumpheapInterval", 60 * 60 * 1000); // one hour
-            mMaxDumpheaps = getLongParam("maxDumpheaps", 5);
-        }
-    }
-
-    private void readProcessesFromFile() {
-        File trackFile = new File(SDCARD + "/track_memory.txt");
-        if (trackFile.exists()) {
-            BufferedReader in = null;
-            try {
-                in = new BufferedReader(new InputStreamReader(new FileInputStream(trackFile)));
-                String processes = in.readLine();
-                in.close();
-
-                if (!"".equals(processes)) {
-                    mProcsToTrack = processes.split(",");
-                }
-            } catch (FileNotFoundException e) {
-                Log.e(TAG, "Error opening track file", e);
-            } catch (IOException e) {
-                Log.e(TAG, "Error opening track file", e);
-            }
-        }
     }
 
     /**
@@ -456,23 +93,52 @@
         mDevice.removeWatcher("LockScreenWatcher");
         mDevice.unfreezeRotation();
 
-        saveMemoryStats();
-
         super.tearDown();
     }
 
-    private class LockScreenWatcher implements UiWatcher {
+    /* *******************  Device Methods ******************* */
 
-        @Override
-        public boolean checkForCondition() {
-            if (mDevice.hasObject(By.desc("Slide area."))) {
-                mDevice.pressMenu();
-                return true;
-            }
-            return false;
+    /**
+     * @deprecated you should be using an AppHelper library to do this.
+     */
+    @Deprecated
+    protected UiDevice getUiDevice() {
+        return mDevice;
+    }
+
+    /**
+     * @deprecated you should be using an AppHelper library to do this.
+     */
+    @Deprecated
+    public void launchIntent(Intent intent) {
+        getInstrumentation().getContext().startActivity(intent);
+    }
+
+    /**
+     * Press back button repeatedly in order to attempt to bring the app back to home screen.
+     * This is intended so that an app can recover if the previous session left an app in a weird
+     * state.
+     *
+     * @deprecated you should be using an AppHelper library to do this.
+     */
+    @Deprecated
+    public void navigateToHome() {
+        int iterations = 0;
+        String launcherPkg = mDevice.getLauncherPackageName();
+        while (!launcherPkg.equals(mDevice.getCurrentPackageName())
+                && iterations < STEPS_BACK) {
+            mDevice.pressBack();
+            SystemClock.sleep(RECOVERY_SLEEP);
+            iterations++;
         }
     }
 
+    /* *******************  Parameter Accessors ******************* */
+
+    protected Bundle getParams() {
+        return ((InstrumentationTestRunner)getInstrumentation()).getArguments();
+    }
+
     /**
      * Looks up a parameter or returns a default value if parameter is not
      * present.
@@ -507,313 +173,18 @@
     }
 
     /**
-     * Press back button repeatedly in order to attempt to bring the app back to home screen.
-     * This is intended so that an app can recover if the previous session left an app in a weird
-     * state.
+     * @return the jar-file arguments of this AUPT test run
      */
-    public void navigateToHome() {
-        int iterations = 0;
-        String launcherPkg = mDevice.getLauncherPackageName();
-        while (!launcherPkg.equals(mDevice.getCurrentPackageName())
-                && iterations < STEPS_BACK) {
-            mDevice.pressBack();
-            SystemClock.sleep(RECOVERY_SLEEP);
-            iterations++;
-        }
+    public List<String> getDexedJarPaths() {
+        return DexTestRunner.parseDexedJarPaths(getParams().getString("jars", ""));
     }
 
     /**
-     * Writes out condensed memory data about the running processes.
-     * @param notes about when the dump was taken.
+     * @return the version corresponding to a given package name
+     *
+     * @deprecated you should be using an AppHelper library to do this.
      */
-    public void dumpMemInfo(String notes) {
-        if (mRecordMeminfo) {
-            mDevice.waitForIdle();
-            mDataCollector.dumpMeminfo(notes);
-        }
-        if (mProcsToTrack != null) {
-            recordMemoryUsage();
-        }
-    }
-
-    private void saveMemoryStats() {
-        if (mProcsToTrack == null) {
-            return;
-        }
-        try {
-            PrintWriter out = new PrintWriter(new BufferedWriter(
-                    new FileWriter(mResultsDirectory + "/memory-health.txt")));
-            out.println("Foreground");
-            for (Map.Entry<String, List<MemHealthRecord>> record : mMemHealthRecords.entrySet()) {
-                List<Long> nativeHeap = MemHealthRecord.getForegroundNativeHeap(record.getValue());
-                List<Long> dalvikHeap = MemHealthRecord.getForegroundDalvikHeap(record.getValue());
-                List<Long> pss = MemHealthRecord.getForegroundPss(record.getValue());
-
-                List<Long> asJavaHeap = MemHealthRecord.getForegroundSummaryJavaHeap(
-                        record.getValue());
-                List<Long> asNativeHeap = MemHealthRecord.getForegroundSummaryNativeHeap(
-                        record.getValue());
-                List<Long> asCode = MemHealthRecord.getForegroundSummaryCode(record.getValue());
-                List<Long> asStack = MemHealthRecord.getForegroundSummaryStack(record.getValue());
-                List<Long> asGraphics = MemHealthRecord.getForegroundSummaryGraphics(
-                        record.getValue());
-                List<Long> asOther = MemHealthRecord.getForegroundSummaryOther(record.getValue());
-                List<Long> asSystem = MemHealthRecord.getForegroundSummarySystem(record.getValue());
-                List<Long> asOverallPss = MemHealthRecord.getForegroundSummaryOverallPss(
-                        record.getValue());
-
-                // nativeHeap, dalvikHeap, and pss all have the same size, just use one
-                if (nativeHeap.size() == 0) {
-                    continue;
-                }
-
-                out.println(record.getKey());
-                out.printf("Average Native Heap: %d\n", MemHealthRecord.getAverage(nativeHeap));
-                out.printf("Average Dalvik Heap: %d\n", MemHealthRecord.getAverage(dalvikHeap));
-                out.printf("Average PSS: %d\n", MemHealthRecord.getAverage(pss));
-                out.printf("Peak Native Heap: %d\n", MemHealthRecord.getMax(nativeHeap));
-                out.printf("Peak Dalvik Heap: %d\n", MemHealthRecord.getMax(dalvikHeap));
-                out.printf("Peak PSS: %d\n", MemHealthRecord.getMax(pss));
-                out.printf("Count %d\n", nativeHeap.size());
-
-                out.printf("Average Summary Java Heap: %d\n", MemHealthRecord.getAverage(
-                        asJavaHeap));
-                out.printf("Average Summary Native Heap: %d\n", MemHealthRecord.getAverage(
-                        asNativeHeap));
-                out.printf("Average Summary Code: %d\n", MemHealthRecord.getAverage(asCode));
-                out.printf("Average Summary Stack: %d\n", MemHealthRecord.getAverage(asStack));
-                out.printf("Average Summary Graphics: %d\n", MemHealthRecord.getAverage(
-                        asGraphics));
-                out.printf("Average Summary Other: %d\n", MemHealthRecord.getAverage(asOther));
-                out.printf("Average Summary System: %d\n", MemHealthRecord.getAverage(asSystem));
-                out.printf("Average Summary Overall Pss: %d\n", MemHealthRecord.getAverage(
-                        asOverallPss));
-            }
-            out.println("Background");
-            for (Map.Entry<String, List<MemHealthRecord>> record : mMemHealthRecords.entrySet()) {
-                List<Long> nativeHeap = MemHealthRecord.getBackgroundNativeHeap(record.getValue());
-                List<Long> dalvikHeap = MemHealthRecord.getBackgroundDalvikHeap(record.getValue());
-                List<Long> pss = MemHealthRecord.getBackgroundPss(record.getValue());
-
-                List<Long> asJavaHeap = MemHealthRecord.getBackgroundSummaryJavaHeap(
-                        record.getValue());
-                List<Long> asNativeHeap = MemHealthRecord.getBackgroundSummaryNativeHeap(
-                        record.getValue());
-                List<Long> asCode = MemHealthRecord.getBackgroundSummaryCode(record.getValue());
-                List<Long> asStack = MemHealthRecord.getBackgroundSummaryStack(record.getValue());
-                List<Long> asGraphics = MemHealthRecord.getBackgroundSummaryGraphics(
-                        record.getValue());
-                List<Long> asOther = MemHealthRecord.getBackgroundSummaryOther(record.getValue());
-                List<Long> asSystem = MemHealthRecord.getBackgroundSummarySystem(record.getValue());
-                List<Long> asOverallPss = MemHealthRecord.getBackgroundSummaryOverallPss(
-                        record.getValue());
-
-                // nativeHeap, dalvikHeap, and pss all have the same size, just use one
-                if (nativeHeap.size() == 0) {
-                    continue;
-                }
-
-                out.println(record.getKey());
-                out.printf("Average Native Heap: %d\n", MemHealthRecord.getAverage(nativeHeap));
-                out.printf("Average Dalvik Heap: %d\n", MemHealthRecord.getAverage(dalvikHeap));
-                out.printf("Average PSS: %d\n", MemHealthRecord.getAverage(pss));
-                out.printf("Peak Native Heap: %d\n", MemHealthRecord.getMax(nativeHeap));
-                out.printf("Peak Dalvik Heap: %d\n", MemHealthRecord.getMax(dalvikHeap));
-                out.printf("Peak PSS: %d\n", MemHealthRecord.getMax(pss));
-                out.printf("Count %d\n", nativeHeap.size());
-
-                out.printf("Average Summary Java Heap: %d\n", MemHealthRecord.getAverage(
-                        asJavaHeap));
-                out.printf("Average Summary Native Heap: %d\n", MemHealthRecord.getAverage(
-                        asNativeHeap));
-                out.printf("Average Summary Code: %d\n", MemHealthRecord.getAverage(asCode));
-                out.printf("Average Summary Stack: %d\n", MemHealthRecord.getAverage(asStack));
-                out.printf("Average Summary Graphics: %d\n", MemHealthRecord.getAverage(
-                        asGraphics));
-                out.printf("Average Summary Other: %d\n", MemHealthRecord.getAverage(asOther));
-                out.printf("Average Summary System: %d\n", MemHealthRecord.getAverage(asSystem));
-                out.printf("Average Summary Overall Pss: %d\n", MemHealthRecord.getAverage(
-                        asOverallPss));
-            }
-            out.close();
-        } catch (IOException e) {
-            Log.e(TAG, "Error while saving memory stats", e);
-        }
-
-        // Temporary hack to write full logs
-        try {
-            PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(
-                    mResultsDirectory + "/memory-health-details.txt")));
-            for (Map.Entry<String, List<MemHealthRecord>> record : mMemHealthRecords.entrySet()) {
-                out.println(record.getKey());
-                out.printf("time,native_heap,dalvik_heap,pss,context\n");
-                for (MemHealthRecord sample : record.getValue()) {
-                    out.printf("%d,%d,%d,%s\n", sample.mTimeMs, sample.mNativeHeap,
-                            sample.mDalvikHeap, sample.mContext.toString().toLowerCase());
-                }
-            }
-            out.close();
-        } catch (IOException e) {
-            Log.e(TAG, "Error while saving memory stat details", e);
-        }
-    }
-
-    private void recordMemoryUsage() {
-        if (mProcsToTrack == null) {
-            return;
-        }
-        long timeMs = System.currentTimeMillis();
-        List<String> foregroundProcs = getForegroundProc();
-        for (String proc : mProcsToTrack) {
-            recordMemoryUsage(proc, timeMs, foregroundProcs);
-        }
-    }
-
-    private void recordMemoryUsage(String proc, long timeMs, List<String> foregroundProcs) {
-        try {
-            String meminfo = getMeminfoOutput(proc);
-            int nativeHeap = parseMeminfoLine(meminfo, "Native Heap\\s+\\d+\\s+(\\d+)");
-            int dalvikHeap = parseMeminfoLine(meminfo, "Dalvik Heap\\s+\\d+\\s+(\\d+)");
-            int pss = parseMeminfoLine(meminfo, "TOTAL\\s+(\\d+)");
-
-            int asJavaHeap = parseMeminfoLine(meminfo, "Java Heap:\\s+(\\d+)");
-            int asNativeHeap = parseMeminfoLine(meminfo, "Native Heap:\\s+(\\d+)");
-            int asCode = parseMeminfoLine(meminfo, "Code:\\s+(\\d+)");
-            int asStack = parseMeminfoLine(meminfo, "Stack:\\s+(\\d+)");
-            int asGraphics = parseMeminfoLine(meminfo, "Graphics:\\s+(\\d+)");
-            int asOther = parseMeminfoLine(meminfo, "Private Other:\\s+(\\d+)");
-            int asSystem = parseMeminfoLine(meminfo, "System:\\s+(\\d+)");
-            int asOverallPss = parseMeminfoLine(meminfo, "TOTAL:\\s+(\\d+)");
-
-            if (nativeHeap < 0 || dalvikHeap < 0 || pss < 0) {
-                return;
-            }
-            MemHealthRecord.Context context = foregroundProcs.contains(proc) ?
-                    MemHealthRecord.Context.FOREGROUND : MemHealthRecord.Context.BACKGROUND;
-            if (!mMemHealthRecords.containsKey(proc)) {
-                mMemHealthRecords.put(proc, new ArrayList<MemHealthRecord>());
-            }
-            mMemHealthRecords.get(proc).add(
-                    new MemHealthRecord(timeMs, dalvikHeap, nativeHeap, pss, asJavaHeap,
-                        asNativeHeap, asCode, asStack, asGraphics, asOther, asSystem,
-                        asOverallPss, context));
-            recordDumpheap(proc, pss);
-        } catch (IOException e) {
-            Log.e(TAG, "exception while memory stats", e);
-        }
-    }
-
-    private int parseMeminfoLine(String meminfo, String pattern)
-    {
-        Pattern p = Pattern.compile(pattern);
-        Matcher m = p.matcher(meminfo);
-        if (m.find()) {
-            return Integer.parseInt(m.group(1));
-        } else {
-            return -1;
-        }
-    }
-
-    private String getMeminfoOutput(String processName) throws IOException {
-        return getProcessOutput("dumpsys meminfo " + processName);
-    }
-
-    private void recordDumpheap(String proc, long pss) throws IOException {
-        if (!mDumpheapEnabled) {
-            return;
-        }
-        Long count = mDumpheapCount.get(proc);
-        if (count == null) {
-            count = 0L;
-        }
-        Long lastDumpheap = mLastDumpheap.get(proc);
-        if (lastDumpheap == null) {
-            lastDumpheap = 0L;
-        }
-        long currentTime = SystemClock.uptimeMillis();
-        if (pss > mDumpheapThreshold && count < mMaxDumpheaps &&
-                currentTime - lastDumpheap > mDumpheapInterval) {
-            recordDumpheap(proc);
-            mDumpheapCount.put(proc, count + 1);
-            mLastDumpheap.put(proc, currentTime);
-        }
-    }
-
-    private void recordDumpheap(String proc) throws IOException {
-        // Turns out getting dumpheap output is non-trivial. The command runs as shell user, and
-        // only has access to /data/local/tmp directory to write files to. The test does not have
-        // access to the output file by default because of the permissions dumpheap sets. So we need
-        // to run dumpheap, change permissions on the output file and copy it to where test harness
-        // can pick it up.
-        Long count = mDumpheapCount.get(proc);
-        if (count == null) {
-            count = 0L;
-        }
-        String filename = String.format("dumpheap-%s-%d", proc, count);
-        String tempFilename = "/data/local/tmp/" + filename;
-        String finalFilename = mResultsDirectory +"/" + filename;
-        String command = String.format("am dumpheap %s %s", proc, tempFilename);
-        getProcessOutput(command);
-        SystemClock.sleep(3000);
-        getProcessOutput(String.format("cp %s %s", tempFilename, finalFilename));
-    }
-
-    public String getProcessOutput(String command) throws IOException {
-        ByteArrayOutputStream baos = new ByteArrayOutputStream();
-        mDataCollector.saveProcessOutput(command, baos);
-        baos.close();
-        return baos.toString();
-    }
-
-    private List<String> getForegroundProc() {
-        List<String> foregroundProcs = new ArrayList<String>();
-        try {
-            String compactMeminfo = getProcessOutput("dumpsys meminfo -c");
-            for (String line : compactMeminfo.split("\\r?\\n")) {
-                if (line.contains("proc,fore")) {
-                    String proc = line.split(",")[2];
-                    foregroundProcs.add(proc);
-                }
-            }
-        } catch (IOException e) {
-            Log.e(TAG, "Error while getting foreground process", e);
-        } finally {
-            return foregroundProcs;
-        }
-    }
-
-    public void setProcessStatusTracker(IProcessStatusTracker processStatusTracker) {
-        mProcessStatusTracker = processStatusTracker;
-    }
-
-    public IProcessStatusTracker getProcessStatusTracker() {
-        return mProcessStatusTracker;
-    }
-
-    public void launchIntent(Intent intent) {
-        getInstrumentation().getContext().startActivity(intent);
-    }
-
-    protected Bundle getParams() {
-        return ((InstrumentationTestRunner)getInstrumentation()).getArguments();
-    }
-
-    protected UiDevice getUiDevice() {
-        return mDevice;
-    }
-
-    public void setDataCollector(DataCollector collector) {
-        mDataCollector = collector;
-    }
-
-    public void setDexedJarPaths(List<String> paths) {
-        mPaths = paths;
-    }
-
-    public List<String> getDexedJarPaths() {
-        return mPaths;
-    }
-
+    @Deprecated
     public String getPackageVersion(String packageName) throws NameNotFoundException {
         if (null == packageName || packageName.isEmpty()) {
               throw new RuntimeException("Package name can't be null or empty");
@@ -833,7 +204,10 @@
      * Get registered accounts
      * Ensures there is at least one account registered
      * returns the google account name
+     *
+     * @deprecated you should be using an AppHelper library to do this.
      */
+    @Deprecated
     public String getRegisteredEmailAccount() {
         Account[] accounts = AccountManager.get(getInstrumentation().getContext()).getAccounts();
         Assert.assertTrue("Device doesn't have any account registered", accounts.length >= 1);
@@ -845,4 +219,24 @@
 
         throw new RuntimeException("The device is not registered with a google account");
     }
+
+    /* *******************  Logging ******************* */
+
+    protected void dumpMemInfo(String notes) {
+        FilesystemUtil.dumpMeminfo(getInstrumentation(), notes);
+    }
+
+    /* *******************  Utilities ******************* */
+
+    private class LockScreenWatcher implements UiWatcher {
+        @Override
+        public boolean checkForCondition() {
+            if (mDevice.hasObject(By.desc("Slide area."))) {
+                mDevice.pressMenu();
+                Log.v(TAG, "Lock screen dismissed.");
+                return true;
+            }
+            return false;
+        }
+    }
 }
diff --git a/libraries/aupt-lib/src/android/support/test/aupt/AuptTestRunner.java b/libraries/aupt-lib/src/android/support/test/aupt/AuptTestRunner.java
index a689a58..136fddf 100644
--- a/libraries/aupt-lib/src/android/support/test/aupt/AuptTestRunner.java
+++ b/libraries/aupt-lib/src/android/support/test/aupt/AuptTestRunner.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2014 The Android Open Source Project
+ * Copyright (C) 2016 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,7 +16,6 @@
 
 package android.support.test.aupt;
 
-import android.app.Instrumentation;
 import android.app.Service;
 import android.content.Context;
 import android.content.ContextWrapper;
@@ -25,185 +24,226 @@
 import android.os.Bundle;
 import android.os.Environment;
 import android.os.IBinder;
+import android.os.SystemClock;
 import android.support.test.uiautomator.UiDevice;
 import android.test.AndroidTestRunner;
 import android.test.InstrumentationTestCase;
 import android.test.InstrumentationTestRunner;
 import android.util.Log;
 
-import dalvik.system.BaseDexClassLoader;
-
 import junit.framework.AssertionFailedError;
 import junit.framework.Test;
 import junit.framework.TestCase;
 import junit.framework.TestListener;
 import junit.framework.TestResult;
+import junit.framework.TestSuite;
 
+import java.io.BufferedReader;
 import java.io.File;
-import java.io.FileOutputStream;
+import java.io.FileInputStream;
 import java.io.IOException;
+import java.io.InputStreamReader;
+import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.Comparator;
-import java.util.concurrent.TimeUnit;
+import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Random;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 
 /**
- * Test runner to use when running AUPT tests.
- * <p>
- * Adds support for multiple iterations, randomizing the order of the tests,
- * terminating tests after UI errors, detecting when processes are getting killed,
- * collecting bugreports and procrank data while the test is running.
+ * Ultra-fancy TestRunner to use when running AUPT: supports
+ *
+ * - Picking up tests from dexed JARs
+ * - Running tests for multiple iterations or in a custom order
+ * - Terminating tests after UI errors, timeouts, or when dependent processes die
+ * - Injecting additional information into custom TestCase subclasses
+ * - Passing through continuous metric-collection to a DataCollector instance
+ * - Collecting bugreports and heapdumps
+ *
  */
 public class AuptTestRunner extends InstrumentationTestRunner {
-    private static final String DEFAULT_JAR_PATH = "/data/local/tmp/";
+    /* Constants */
+    private static final String LOG_TAG = AuptTestRunner.class.getSimpleName();
+    private static final Long ANR_DELAY = 30000L;
+    private static final Long DEFAULT_SUITE_TIMEOUT = 0L;
+    private static final Long DEFAULT_TEST_TIMEOUT = 10L;
+    private static final SimpleDateFormat SCREENSHOT_DATE_FORMAT =
+        new SimpleDateFormat("dd-mm-yy:HH:mm:ss:SSS");
 
-    private static final String LOG_TAG = "AuptTestRunner";
-    private static final String DEX_OPT_PATH = "aupt-opt";
-    private static final String PARAM_JARS = "jars";
+    /* Keep a pointer to our argument bundle around for testing */
     private Bundle mParams;
 
-    private long mIterations;
-    private Random mRandom;
-    private boolean mShuffle;
-    private boolean mGenerateAnr;
-    private long mTestCaseTimeout = TimeUnit.MINUTES.toMillis(10);
-    private DataCollector mDataCollector;
-    private File mResultsDirectory;
-
+    /* Primitive Parameters */
     private boolean mDeleteOldFiles;
     private long mFileRetainCount;
+    private boolean mGenerateAnr;
+    private boolean mRecordMeminfo;
+    private long mIterations;
+    private long mSeed;
 
-    private AuptPrivateTestRunner mRunner = new AuptPrivateTestRunner();
-    private ClassLoader mLoader = null;
-    private Context mTargetContextWrapper;
+    /* Dumpheap Parameters */
+    private boolean mDumpheapEnabled;
+    private long mDumpheapInterval;
+    private long mDumpheapThreshold;
+    private long mMaxDumpheaps;
 
-    private IProcessStatusTracker mProcessTracker;
+    /* String Parameters */
+    private List<String> mJars = new ArrayList<>();
+    private List<String> mMemoryTrackedProcesses = new ArrayList<>();
+    private List<String> mFinishCommands;
 
-    private Map<String, List<AuptTestCase.MemHealthRecord>> mMemHealthRecords;
+    /* Other Parameters */
+    private File mResultsDirectory;
 
-    private boolean mTrackJank;
-    private GraphicsStatsMonitor mGraphicsStatsMonitor;
-    private List<String> mJars;
+    /* Helpers */
+    private Scheduler mScheduler;
+    private DataCollector mDataCollector;
+    private DexTestRunner mRunner;
 
-    /**
-     * {@inheritDoc}
-     */
+    /* Logging */
+    private ProcessStatusTracker mProcessTracker;
+    private List<MemHealthRecord> mMemHealthRecords = new ArrayList<>();
+    private Map<String, Long> mDumpheapCount = new HashMap<>();
+    private Map<String, Long> mLastDumpheap = new HashMap<>();
+
+    /* Test Initialization */
     @Override
     public void onCreate(Bundle params) {
-
         mParams = params;
 
-        mMemHealthRecords = new HashMap<String, List<AuptTestCase.MemHealthRecord>>();
-
+        // Parse out primitive parameters
         mIterations = parseLongParam("iterations", 1);
-        mShuffle = parseBooleanParam("shuffle", false);
-        long seed = parseLongParam("seed", (new Random()).nextLong());
-        Log.d(LOG_TAG, String.format("Using seed value: %s", seed));
-        mRandom = new Random(seed);
-        // set to 'generateANR to 'true' when more info required for debugging on test timeout'
-        mGenerateAnr = parseBooleanParam("generateANR", false);
-        if (parseBooleanParam("quitOnError", false)) {
-            mRunner.addTestListener(new QuitOnErrorListener());
+        mRecordMeminfo = parseBoolParam("record_meminfo", false);
+        mDumpheapEnabled = parseBoolParam("enableDumpheap", false);
+        mDumpheapThreshold = parseLongParam("dumpheapThreshold", 200 * 1024 * 1024);
+        mDumpheapInterval = parseLongParam("dumpheapInterval", 60 * 60 * 1000);
+        mMaxDumpheaps = parseLongParam("maxDumpheaps", 5);
+        mSeed = parseLongParam("seed", new Random().nextLong());
+
+        // Option: -e finishCommand 'a;b;c;d'
+        String finishCommandArg = parseStringParam("finishCommand", null);
+        mFinishCommands =
+                finishCommandArg == null
+                        ? Arrays.<String>asList()
+                        : Arrays.asList(finishCommandArg.split("\\s*;\\s*"));
+
+        // Option: -e shuffle true
+        mScheduler = parseBoolParam("shuffle", false)
+                ? Scheduler.shuffled(new Random(mSeed), mIterations)
+                : Scheduler.sequential(mIterations);
+
+        // Option: -e jars aupt-app-tests.jar:...
+        mJars.addAll(DexTestRunner.parseDexedJarPaths(parseStringParam("jars", "")));
+
+        // Option: -e trackMemory com.pkg1,com.pkg2,...
+        String memoryTrackedProcesses = parseStringParam("trackMemory", null);
+
+        if (memoryTrackedProcesses != null) {
+            mMemoryTrackedProcesses = Arrays.asList(memoryTrackedProcesses.split(","));
+        } else {
+            try {
+                // Deprecated approach: get tracked processes from a file.
+                String trackMemoryFileName =
+                        Environment.getExternalStorageDirectory() + "/track_memory.txt";
+
+                BufferedReader reader = new BufferedReader(new InputStreamReader(
+                        new FileInputStream(new File(trackMemoryFileName))));
+
+                mMemoryTrackedProcesses = Arrays.asList(reader.readLine().split(","));
+                reader.close();
+            } catch (NullPointerException | IOException ex) {
+                mMemoryTrackedProcesses = Arrays.asList();
+            }
         }
-        if (parseBooleanParam("checkBattery", false)) {
-            mRunner.addTestListener(new BatteryChecker());
-        }
-        mTestCaseTimeout = parseLongParam("testCaseTimeout", mTestCaseTimeout);
 
         // Option: -e detectKill com.pkg1,...,com.pkg8
         String processes = parseStringParam("detectKill", null);
+
         if (processes != null) {
             mProcessTracker = new ProcessStatusTracker(processes.split(","));
         } else {
-            mProcessTracker = new ProcessStatusTracker(null);
+            mProcessTracker = new ProcessStatusTracker(new String[] {});
         }
 
-        // Option: -e jankInterval integer
-        long interval = parseLongParam("jankInterval", -1L);
-        if (interval > 0) {
-            mTrackJank = true;
-            mGraphicsStatsMonitor = new GraphicsStatsMonitor();
-            mGraphicsStatsMonitor.setIntervalRate(interval);
-        }
-
-        mRunner.addTestListener(new PidChecker());
+        // Option: -e outputLocation aupt_results
         mResultsDirectory = new File(Environment.getExternalStorageDirectory(),
                 parseStringParam("outputLocation", "aupt_results"));
         if (!mResultsDirectory.exists() && !mResultsDirectory.mkdirs()) {
-            Log.w(LOG_TAG, "Did not create output directory");
+            Log.w(LOG_TAG, "Could not find or create output directory " + mResultsDirectory);
         }
 
+        // Option: -e fileRetainCount 1
         mFileRetainCount = parseLongParam("fileRetainCount", -1);
-        if (mFileRetainCount == -1) {
-            mDeleteOldFiles = false;
-        } else {
-            mDeleteOldFiles = true;
-        }
+        mDeleteOldFiles = (mFileRetainCount != -1);
 
+        // Primary logging infrastructure
         mDataCollector = new DataCollector(
                 TimeUnit.MINUTES.toMillis(parseLongParam("bugreportInterval", 0)),
+                TimeUnit.MINUTES.toMillis(parseLongParam("jankInterval", 0)),
                 TimeUnit.MINUTES.toMillis(parseLongParam("meminfoInterval", 0)),
                 TimeUnit.MINUTES.toMillis(parseLongParam("cpuinfoInterval", 0)),
                 TimeUnit.MINUTES.toMillis(parseLongParam("fragmentationInterval", 0)),
                 TimeUnit.MINUTES.toMillis(parseLongParam("ionInterval", 0)),
                 TimeUnit.MINUTES.toMillis(parseLongParam("pagetypeinfoInterval", 0)),
                 TimeUnit.MINUTES.toMillis(parseLongParam("traceInterval", 0)),
+                TimeUnit.MINUTES.toMillis(parseLongParam("bugreportzInterval", 0)),
                 mResultsDirectory, this);
-        String jars = params.getString(PARAM_JARS);
-        if (jars != null) {
-            loadDexJars(jars);
-        }
-        mTargetContextWrapper = new ClassLoaderContextWrapper();
+
+        // Make our TestRunner and make sure we injectInstrumentation.
+        mRunner = new DexTestRunner(this, mScheduler, mJars,
+                TimeUnit.MINUTES.toMillis(parseLongParam("testCaseTimeout", DEFAULT_TEST_TIMEOUT)),
+                TimeUnit.MINUTES.toMillis(parseLongParam("suiteTimeout", DEFAULT_SUITE_TIMEOUT))) {
+            @Override
+            public void runTest(TestResult result) {
+                for (TestCase test: mTestCases) {
+                    injectInstrumentation(test);
+                }
+
+                super.runTest(result);
+            }
+        };
+
+        // Aupt's TestListeners
+        mRunner.addTestListener(new PeriodicHeapDumper());
+        mRunner.addTestListener(new MemHealthRecorder());
+        mRunner.addTestListener(new DcimCleaner());
+        mRunner.addTestListener(new PidChecker());
+        mRunner.addTestListener(new TimeoutStackDumper());
+        mRunner.addTestListener(new MemInfoDumper());
+        mRunner.addTestListener(new FinishCommandRunner());
+        mRunner.addTestListenerIf(parseBoolParam("generateANR", false), new ANRTrigger());
+        mRunner.addTestListenerIf(parseBoolParam("quitOnError", false), new QuitOnErrorListener());
+        mRunner.addTestListenerIf(parseBoolParam("checkBattery", false), new BatteryChecker());
+        mRunner.addTestListenerIf(parseBoolParam("screenshots", false), new Screenshotter());
+
+        // Start our loggers
+        mDataCollector.start();
+
+        // Start the test
         super.onCreate(params);
     }
 
-    private void loadDexJars(String jars) {
-        // scan provided jar paths, translate relative to absolute paths, and check for existence
-        String[] jarsArray = jars.split(":");
-        StringBuilder classLoaderPath = new StringBuilder();
-        mJars = new ArrayList<String>();
-        for (int i = 0; i < jarsArray.length; i++) {
-            String jar = jarsArray[i];
-            if (!jar.startsWith("/")) {
-                jar = DEFAULT_JAR_PATH + jar;
-            }
-            File jarFile = new File(jar);
-            if (!jarFile.exists() || !jarFile.canRead()) {
-                throw new IllegalArgumentException("Jar file does not exist or not accessible: "
-                        + jar);
-            }
-            if (i != 0) {
-                classLoaderPath.append(File.pathSeparator);
-            }
-            classLoaderPath.append(jarFile.getAbsolutePath());
-            mJars.add(jarFile.getAbsolutePath());
-        }
-        // now load them
-        File optDir = new File(getContext().getCacheDir(), DEX_OPT_PATH);
-        if (!optDir.exists() && !optDir.mkdirs()) {
-            throw new RuntimeException(
-                    "Failed to create dex optimize directory: " + optDir.getAbsolutePath());
-        }
-        mLoader = new BaseDexClassLoader(classLoaderPath.toString(), optDir, null,
-                super.getTargetContext().getClassLoader());
-
+    @Override
+    public void onDestroy() {
+        mDataCollector.stop();
     }
 
+    /* Option-parsing helpers */
+
     private long parseLongParam(String key, long alternative) throws NumberFormatException {
         if (mParams.containsKey(key)) {
-            return Long.parseLong(
-                    mParams.getString(key));
+            return Long.parseLong(mParams.getString(key));
         } else {
             return alternative;
         }
     }
 
-    private boolean parseBooleanParam(String key, boolean alternative)
+    private boolean parseBoolParam(String key, boolean alternative)
             throws NumberFormatException {
         if (mParams.containsKey(key)) {
             return Boolean.parseBoolean(mParams.getString(key));
@@ -220,244 +260,284 @@
         }
     }
 
-    private void writeProgressMessage(String msg) {
-        writeMessage("progress.txt", msg);
-    }
+    /* Utility methods */
 
-    private void writeGraphicsMessage(String msg) {
-        writeMessage("graphics.txt", msg);
-    }
+    /**
+     * Injects instrumentation into InstrumentationTestCase and AuptTestCase instances
+     */
+    private void injectInstrumentation(Test test) {
+        if (InstrumentationTestCase.class.isAssignableFrom(test.getClass())) {
+            InstrumentationTestCase instrTest = (InstrumentationTestCase) test;
 
-    private void writeMessage(String filename, String msg) {
-        try {
-            FileOutputStream fos = new FileOutputStream(
-                    new File(mResultsDirectory, filename));
-            fos.write(msg.getBytes());
-            fos.flush();
-            fos.close();
-        } catch (IOException ioe) {
-            Log.e(LOG_TAG, "error saving progress file", ioe);
+            instrTest.injectInstrumentation(AuptTestRunner.this);
         }
     }
 
-    /**
-     * Provide a wrapped context so that we can provide an alternative class loader
-     * @return
-     */
-    @Override
-    public Context getTargetContext() {
-        return mTargetContextWrapper;
-    }
-
-    /**
-     * {@inheritDoc}
-     */
+    /* Passthrough to our DexTestRunner */
     @Override
     protected AndroidTestRunner getAndroidTestRunner() {
-        // AndroidTestRunner is what determines which tests get to run.
-        // Unfortunately there is no hooks into it, so most of
-        // the functionality has to be duplicated
         return mRunner;
     }
 
-    /**
-     * Sets up and starts monitoring jank metrics by clearing the currently existing data.
-     */
-    private void startJankMonitoring () {
-        if (mTrackJank) {
-            mGraphicsStatsMonitor.setUiAutomation(getUiAutomation());
-            mGraphicsStatsMonitor.startMonitoring();
-
-            // TODO: Clear graphics.txt file if extant
-        }
-    }
-
-    /**
-     * Stops future monitoring of jank metrics, but preserves current metrics intact.
-     */
-    private void stopJankMonitoring () {
-        if (mTrackJank) {
-            mGraphicsStatsMonitor.stopMonitoring();
-        }
-    }
-
-    /**
-     * Aggregates and merges jank metrics and writes them to the graphics file.
-     */
-    private void writeJankMetrics () {
-        if (mTrackJank) {
-            List<JankStat> mergedStats = mGraphicsStatsMonitor.aggregateStatsImages();
-            String mergedStatsString = JankStat.statsListToString(mergedStats);
-
-            Log.d(LOG_TAG, "Writing jank metrics to the graphics file");
-            writeGraphicsMessage(mergedStatsString);
-        }
-    }
-
-    /**
-     * Determines which tests to run, configures the test class and then runs the test.
-     */
-    private class AuptPrivateTestRunner extends AndroidTestRunner {
-
-        private List<TestCase> mTestCases;
-        private List<TestListener> mTestListeners = new ArrayList<>();
-        private Instrumentation mInstrumentation;
-        private TestResult mTestResult;
-
-        @Override
-        public List<TestCase> getTestCases() {
-            if (mTestCases != null) {
-                return mTestCases;
-            }
-
-            List<TestCase> testCases = new ArrayList<TestCase>(super.getTestCases());
-            List<TestCase> completeList = new ArrayList<TestCase>();
-
-            for (int i = 0; i < mIterations; i++) {
-                if (mShuffle) {
-                    Collections.shuffle(testCases, mRandom);
+    @Override
+    public Context getTargetContext() {
+        return new ContextWrapper(super.getTargetContext()) {
+            @Override
+            public ClassLoader getClassLoader() {
+                if(mRunner != null) {
+                    return mRunner.getDexClassLoader();
+                } else {
+                    throw new RuntimeException("DexTestRunner not initialized!");
                 }
-                completeList.addAll(testCases);
+            }
+        };
+    }
+
+    /**
+     * A simple abstract instantiation of TestListener
+     *
+     * Primarily meant to work around Java 7's lack of interface-default methods.
+     */
+    abstract static class AuptListener implements TestListener {
+        /** Called when a test throws an exception. */
+        public void addError(Test test, Throwable t) {}
+
+        /** Called when a test fails. */
+        public void addFailure(Test test, AssertionFailedError t) {}
+
+        /** Called whenever a test ends. */
+        public void endTest(Test test) {}
+
+        /** Called whenever a test begins. */
+        public void startTest(Test test) {}
+    }
+
+    /**
+     * Periodically Heap-dump to assist with memory-leaks.
+     */
+    private class PeriodicHeapDumper extends AuptListener {
+        private Thread mHeapDumpThread;
+
+        private class InternalHeapDumper implements Runnable {
+            private void recordDumpheap(String proc, long pss) throws IOException {
+                if (!mDumpheapEnabled) {
+                    return;
+                }
+                Long count = mDumpheapCount.get(proc);
+                if (count == null) {
+                    count = 0L;
+                }
+                Long lastDumpheap = mLastDumpheap.get(proc);
+                if (lastDumpheap == null) {
+                    lastDumpheap = 0L;
+                }
+                long currentTime = SystemClock.uptimeMillis();
+                if (pss > mDumpheapThreshold && count < mMaxDumpheaps &&
+                        currentTime - lastDumpheap > mDumpheapInterval) {
+                    recordDumpheap(proc);
+                    mDumpheapCount.put(proc, count + 1);
+                    mLastDumpheap.put(proc, currentTime);
+                }
             }
 
-            mTestCases = completeList;
-            return mTestCases;
-        }
+            private void recordDumpheap(String proc) throws IOException {
+                long count = mDumpheapCount.get(proc);
 
-        @Override
-        public void runTest(TestResult testResult) {
-            mTestResult = testResult;
+                String filename = String.format("dumpheap-%s-%d", proc, count);
+                String tempFilename = "/data/local/tmp/" + filename;
+                String finalFilename = mResultsDirectory + "/" + filename;
 
-            ((ProcessStatusTracker)mProcessTracker).setUiAutomation(getUiAutomation());
+                AuptTestRunner.this.getUiAutomation().executeShellCommand(
+                        String.format("am dumpheap %s %s", proc, tempFilename));
 
-            mDataCollector.start();
-            startJankMonitoring();
+                SystemClock.sleep(3000);
 
-            for (TestListener testListener : mTestListeners) {
-                mTestResult.addListener(testListener);
+                AuptTestRunner.this.getUiAutomation().executeShellCommand(
+                        String.format("cp %s %s", tempFilename, finalFilename));
             }
 
-            Runnable timeBomb = new Runnable() {
-                @Override
-                public void run() {
-                    try {
-                        Thread.sleep(mTestCaseTimeout);
-                    } catch (InterruptedException e) {
-                        return;
-                    }
-                    // if we ever wake up, a timeout has occurred, set off the bomb,
-                    // but trigger a service ANR first
-                    if (mGenerateAnr) {
-                        Context ctx = getTargetContext();
-                        Log.d(LOG_TAG, "About to induce artificial ANR for debugging");
-                        ctx.startService(new Intent(ctx, BadService.class));
-                        // intentional delay to allow the service ANR to happen then resolve
-                        try {
-                            Thread.sleep(BadService.DELAY + BadService.DELAY / 4);
-                        } catch (InterruptedException e) {
-                            // ignore
-                            Log.d(LOG_TAG, "interrupted in wait on BadService");
-                            return;
+            public void run() {
+                try {
+                    while (true) {
+                        Thread.sleep(mDumpheapInterval);
+
+                        for(String proc : mMemoryTrackedProcesses) {
+                            recordDumpheap(proc);
                         }
-                    } else {
-                        Log.d("THREAD_DUMP", getStackTraces());
                     }
-                    throw new RuntimeException(String.format("max testcase timeout exceeded: %s ms",
-                            mTestCaseTimeout));
+                } catch (InterruptedException iex) {
+                } catch (IOException ioex) {
+                    Log.e(LOG_TAG, "Failed to write heap dump!", ioex);
                 }
-            };
+            }
+        }
+
+        @Override
+        public void startTest(Test test) {
+            mHeapDumpThread = new Thread(new InternalHeapDumper());
+            mHeapDumpThread.start();
+        }
+
+        @Override
+        public void endTest(Test test) {
+            try {
+                mHeapDumpThread.interrupt();
+                mHeapDumpThread.join();
+            } catch (InterruptedException iex) { }
+        }
+    }
+
+    /**
+     * Dump memory info on test start/stop
+     */
+    private class MemInfoDumper extends AuptListener {
+        private void dumpMemInfo() {
+            if (mRecordMeminfo) {
+                FilesystemUtil.dumpMeminfo(AuptTestRunner.this, "MemInfoDumper");
+            }
+        }
+
+        @Override
+        public void startTest(Test test) {
+            dumpMemInfo();
+        }
+
+        @Override
+        public void endTest(Test test) {
+            dumpMemInfo();
+        }
+    }
+
+    /**
+     * Record all of our MemHealthRecords
+     */
+    private class MemHealthRecorder extends AuptListener {
+        @Override
+        public void startTest(Test test) {
+            recordMemHealth();
+        }
+
+        @Override
+        public void endTest(Test test) {
+            recordMemHealth();
 
             try {
-                // Try to run all TestCases, but ensure the finally block is reached
-                for (TestCase testCase : mTestCases) {
-                    setInstrumentationIfInstrumentationTestCase(testCase, mInstrumentation);
-                    setupAuptIfAuptTestCase(testCase);
+                MemHealthRecord.saveVerbose(mMemHealthRecords,
+                    mResultsDirectory + "memory-health.txt");
 
-                    // Remove device storage as necessary
-                    removeOldImagesFromDcimCameraFolder();
+                MemHealthRecord.saveCsv(mMemHealthRecords,
+                    mResultsDirectory + "memory-health-details.txt");
 
-                    Thread timeBombThread = null;
-                    if (mTestCaseTimeout > 0) {
-                        timeBombThread = new Thread(timeBomb);
-                        timeBombThread.setName("Boom!");
-                        timeBombThread.setDaemon(true);
-                        timeBombThread.start();
-                    }
+                mMemHealthRecords.clear();
+            } catch (IOException ioex) {
+                Log.e(LOG_TAG, "Error writing MemHealthRecords", ioex);
+            }
+        }
 
-                    try {
-                        testCase.run(mTestResult);
-                    } catch (AuptTerminator ex) {
-                        // Write to progress.txt to pass the exception message to the dashboard
-                        writeProgressMessage("Exception: " + ex);
-                        // Throw the exception, because we want to discontinue running tests
-                        throw ex;
-                    }
+        private void recordMemHealth() {
+            try {
+                mMemHealthRecords.addAll(MemHealthRecord.get(
+                      AuptTestRunner.this,
+                      mMemoryTrackedProcesses,
+                      System.currentTimeMillis(),
+                      getForegroundProcs()));
+            } catch (IOException ioex) {
+                Log.e(LOG_TAG, "Error collecting MemHealthRecords", ioex);
+            }
+        }
 
-                    if (mTestCaseTimeout > 0) {
-                        timeBombThread.interrupt();
-                        try {
-                            timeBombThread.join();
-                        } catch (InterruptedException e) {
-                            // ignore
-                        }
+        private List<String> getForegroundProcs() {
+            List<String> foregroundProcs = new ArrayList<String>();
+            try {
+                String compactMeminfo = MemHealthRecord.getProcessOutput(AuptTestRunner.this,
+                        "dumpsys meminfo -c");
+
+                for (String line : compactMeminfo.split("\\r?\\n")) {
+                    if (line.contains("proc,fore")) {
+                        String proc = line.split(",")[2];
+                        foregroundProcs.add(proc);
                     }
                 }
+            } catch (IOException e) {
+                Log.e(LOG_TAG, "Error while getting foreground process", e);
             } finally {
-                // Ensure the DataCollector ends all dangling Threads
-                mDataCollector.stop();
-                // Ensure the Timer in GraphicsStatsMonitor is canceled
-                stopJankMonitoring(); // However, it is daemon
-                // Ensure jank metrics are written to the graphics file
-                writeJankMetrics();
+                return foregroundProcs;
             }
         }
+    }
 
-        /**
-         * Gets all thread stack traces.
-         *
-         * @return string of all thread stack traces
-         */
-        private String getStackTraces() {
-            StringBuilder sb = new StringBuilder();
-            Map<Thread, StackTraceElement[]> stacks = Thread.getAllStackTraces();
-            for (Thread t : stacks.keySet()) {
-                sb.append(t.toString()).append('\n');
-                for (StackTraceElement ste : t.getStackTrace()) {
-                    sb.append("\tat ").append(ste.toString()).append('\n');
+    /**
+     * Kills application and dumps UI Hierarchy on test error
+     */
+    private class QuitOnErrorListener extends AuptListener {
+        @Override
+        public void addError(Test test, Throwable t) {
+            Log.e(LOG_TAG, "Caught exception from a test", t);
+
+            if ((t instanceof AuptTerminator)) {
+                throw (AuptTerminator)t;
+            } else {
+
+                // Check if our exception is caused by process dependency
+                if (test instanceof AuptTestCase) {
+                    mProcessTracker.setUiAutomation(getUiAutomation());
+                    mProcessTracker.verifyRunningProcess();
                 }
-                sb.append('\n');
+
+                // If that didn't throw, then dump our hierarchy
+                Log.v(LOG_TAG, "Dumping UI hierarchy");
+                try {
+                    UiDevice.getInstance(AuptTestRunner.this).dumpWindowHierarchy(
+                            new File("/data/local/tmp/error_dump.xml"));
+                } catch (IOException e) {
+                    Log.w(LOG_TAG, "Failed to create UI hierarchy dump for UI error", e);
+                }
             }
-            return sb.toString();
+
+            // Quit on an error
+            throw new AuptTerminator(t.getMessage(), t);
         }
 
-        private void setInstrumentationIfInstrumentationTestCase(
-                Test test, Instrumentation instrumentation) {
-            if (InstrumentationTestCase.class.isAssignableFrom(test.getClass())) {
-                ((InstrumentationTestCase) test).injectInstrumentation(instrumentation);
-            }
+        @Override
+        public void addFailure(Test test, AssertionFailedError t) {
+            // Quit on an error
+            throw new AuptTerminator(t.getMessage(), t);
+        }
+    }
+
+    /**
+     * Makes sure the processes this test requires are all alive
+     */
+    private class PidChecker extends AuptListener {
+        @Override
+        public void startTest(Test test) {
+            mProcessTracker.setUiAutomation(getUiAutomation());
+            mProcessTracker.verifyRunningProcess();
         }
 
-        // Aupt specific set up.
-        private void setupAuptIfAuptTestCase(Test test) {
-            if (test instanceof AuptTestCase){
-                ((AuptTestCase)test).setProcessStatusTracker(mProcessTracker);
-                ((AuptTestCase)test).setMemHealthRecords(mMemHealthRecords);
-                ((AuptTestCase)test).setDataCollector(mDataCollector);
-                ((AuptTestCase)test).setDexedJarPaths(mJars);
-            }
+        @Override
+        public void endTest(Test test) {
+            mProcessTracker.verifyRunningProcess();
         }
+    }
 
-        private void removeOldImagesFromDcimCameraFolder() {
+    /**
+     * Initialization for tests that touch the camera
+     */
+    private class DcimCleaner extends AuptListener {
+        @Override
+        public void startTest(Test test) {
             if (!mDeleteOldFiles) {
                 return;
             }
 
             File dcimFolder = new File(Environment.getExternalStorageDirectory(), "DCIM");
-            if (dcimFolder != null) {
-                File cameraFolder = new File(dcimFolder, "Camera");
-                if (cameraFolder != null) {
+            File cameraFolder = new File(dcimFolder, "Camera");
+
+            if (dcimFolder.exists()) {
+                if (cameraFolder.exists()) {
                     File[] allMediaFiles = cameraFolder.listFiles();
-                    Arrays.sort(allMediaFiles, new Comparator<File> () {
+                    Arrays.sort(allMediaFiles, new Comparator<File>() {
                         public int compare(File f1, File f2) {
                             return Long.valueOf(f1.lastModified()).compareTo(f2.lastModified());
                         }
@@ -472,109 +552,12 @@
                 Log.w(LOG_TAG, "No DCIM folder found to delete from.");
             }
         }
-
-        @Override
-        public void clearTestListeners() {
-            mTestListeners.clear();
-        }
-
-        @Override
-        public void addTestListener(TestListener testListener) {
-            if (testListener != null) {
-                mTestListeners.add(testListener);
-            }
-        }
-
-        @Override
-        public void setInstrumentation(Instrumentation instrumentation) {
-            mInstrumentation = instrumentation;
-        }
-
-        @Override
-        public TestResult getTestResult() {
-            return mTestResult;
-        }
-
-        @Override
-        protected TestResult createTestResult() {
-            return new TestResult();
-        }
     }
 
     /**
-     * Test listener that monitors the AUPT tests for any errors. If the option is set it will
-     * terminate the whole test run if it encounters an exception.
+     * Makes sure the battery hasn't died before and after each test.
      */
-    private class QuitOnErrorListener implements TestListener {
-
-        @Override
-        public void addError(Test test, Throwable t) {
-            Log.e(LOG_TAG, "Caught exception from a test", t);
-            if ((t instanceof AuptTerminator)) {
-                throw (AuptTerminator)t;
-            } else {
-                // check that if the UI exception is caused by process getting killed
-                if (test instanceof AuptTestCase) {
-                    ((AuptTestCase)test).getProcessStatusTracker().verifyRunningProcess();
-                }
-                // if previous line did not throw an exception, we are interested to know what
-                // caused the UI exception
-                Log.v(LOG_TAG, "Dumping UI hierarchy");
-                try {
-                    UiDevice.getInstance(AuptTestRunner.this).dumpWindowHierarchy(
-                            new File("/data/local/tmp/error_dump.xml"));
-                } catch (IOException e) {
-                    Log.w(LOG_TAG, "Failed to create UI hierarchy dump for UI error", e);
-                }
-            }
-
-            throw new AuptTerminator(t.getMessage(), t);
-        }
-
-        @Override
-        public void addFailure(Test test, AssertionFailedError t) {
-            throw new AuptTerminator(t.getMessage(), t);
-        }
-
-        @Override
-        public void endTest(Test test) {
-            // skip
-        }
-
-        @Override
-        public void startTest(Test test) {
-            // skip
-        }
-    }
-
-    /**
-     * A listener that checks that none of the monitored processes died during the test.
-     * If a process dies it will  terminate the test early.
-     */
-    private class PidChecker implements TestListener {
-
-        @Override
-        public void addError(Test test, Throwable t) {
-            // no-op
-        }
-
-        @Override
-        public void addFailure(Test test, AssertionFailedError t) {
-            // no-op
-        }
-
-        @Override
-        public void endTest(Test test) {
-            mProcessTracker.verifyRunningProcess();
-        }
-
-        @Override
-        public void startTest(Test test) {
-            mProcessTracker.verifyRunningProcess();
-        }
-    }
-
-    private class BatteryChecker implements TestListener {
+    private class BatteryChecker extends AuptListener {
         private static final double BATTERY_THRESHOLD = 0.05;
 
         private void checkBattery() {
@@ -596,62 +579,120 @@
         }
 
         @Override
-        public void addError(Test test, Throwable t) {
-            // skip
-        }
-
-        @Override
-        public void addFailure(Test test, AssertionFailedError afe) {
-            // skip
-        }
-
-        @Override
-        public void endTest(Test test) {
-            // skip
-        }
-
-        @Override
         public void startTest(Test test) {
             checkBattery();
         }
     }
 
     /**
-     * A {@link ContextWrapper} that overrides {@link Context#getClassLoader()}
+     * Generates heap dumps when a test times out
      */
-    class ClassLoaderContextWrapper extends ContextWrapper {
-
-        public ClassLoaderContextWrapper() {
-            super(AuptTestRunner.super.getTargetContext());
+    private class TimeoutStackDumper extends AuptListener {
+        private String getStackTraces() {
+            StringBuilder sb = new StringBuilder();
+            Map<Thread, StackTraceElement[]> stacks = Thread.getAllStackTraces();
+            for (Thread t : stacks.keySet()) {
+                sb.append(t.toString()).append('\n');
+                for (StackTraceElement ste : t.getStackTrace()) {
+                    sb.append("\tat ").append(ste.toString()).append('\n');
+                }
+                sb.append('\n');
+            }
+            return sb.toString();
         }
 
-        /**
-         * Alternatively returns a custom class loader with classes loaded from additional jars
-         */
         @Override
-        public ClassLoader getClassLoader() {
-            if (mLoader != null) {
-                return mLoader;
-            } else {
-                return super.getClassLoader();
+        public void addError(Test test, Throwable t) {
+            if (t instanceof TimeoutException) {
+                Log.d("THREAD_DUMP", getStackTraces());
             }
         }
     }
 
-    public static class BadService extends Service {
-        public static final long DELAY = 30000;
+    /** Generates ANRs when a test takes too long. */
+    private class ANRTrigger extends AuptListener {
         @Override
-        public IBinder onBind(Intent intent) {
-            return null;
+        public void addError(Test test, Throwable t) {
+            if (t instanceof TimeoutException) {
+                Context ctx = getTargetContext();
+                Log.d(LOG_TAG, "About to induce artificial ANR for debugging");
+                ctx.startService(new Intent(ctx, AnrGenerator.class));
+
+                try {
+                    Thread.sleep(ANR_DELAY);
+                } catch (InterruptedException e) {
+                    throw new RuntimeException("Interrupted while waiting for AnrGenerator...");
+                }
+            }
+        }
+
+        /** Service that hangs to trigger an ANR. */
+        private class AnrGenerator extends Service {
+            @Override
+            public IBinder onBind(Intent intent) {
+                return null;
+            }
+
+            @Override
+            public int onStartCommand(Intent intent, int flags, int id) {
+                Log.i(LOG_TAG, "in service start -- about to hang");
+                try {
+                    Thread.sleep(ANR_DELAY);
+                } catch (InterruptedException e) {
+                    Log.wtf(LOG_TAG, e);
+                }
+                Log.i(LOG_TAG, "service hang finished -- stopping and returning");
+                stopSelf();
+                return START_NOT_STICKY;
+            }
+        }
+    }
+
+    /**
+     * Collect a screenshot on test failure.
+     */
+    private class Screenshotter extends AuptListener {
+        private void collectScreenshot(Test test, String suffix) {
+            UiDevice device = UiDevice.getInstance(AuptTestRunner.this);
+
+            if (device == null) {
+                Log.w(LOG_TAG, "Couldn't collect screenshot on test failure");
+                return;
+            }
+
+            String testName =
+                    test instanceof TestCase
+                    ? ((TestCase) test).getName()
+                    : (test instanceof TestSuite ? ((TestSuite) test).getName() : test.toString());
+
+            String fileName =
+                    mResultsDirectory.getPath()
+                            + "/" + testName.replaceAll(".", "_")
+                            + suffix + ".png";
+
+            device.takeScreenshot(new File(fileName));
         }
 
         @Override
-        public int onStartCommand(Intent intent, int flags, int id) {
-            Log.i(LOG_TAG, "in service start -- about to hang");
-            try { Thread.sleep(DELAY); } catch (InterruptedException e) { Log.wtf(LOG_TAG, e); }
-            Log.i(LOG_TAG, "service hang finished -- stopping and returning");
-            stopSelf();
-            return START_NOT_STICKY;
+        public void addError(Test test, Throwable t) {
+            collectScreenshot(test,
+                    "_failure_screenshot_" + SCREENSHOT_DATE_FORMAT.format(new Date()));
+        }
+
+        @Override
+        public void addFailure(Test test, AssertionFailedError t) {
+            collectScreenshot(test,
+                    "_failure_screenshot_" + SCREENSHOT_DATE_FORMAT.format(new Date()));
+        }
+    }
+
+    /** Runs a command when a test finishes. */
+    private class FinishCommandRunner extends AuptListener {
+        @Override
+        public void endTest(Test test) {
+            for (String command : mFinishCommands) {
+                AuptTestRunner.this.getUiAutomation().executeShellCommand(command);
+            }
         }
     }
 }
diff --git a/libraries/aupt-lib/src/android/support/test/aupt/DataCollector.java b/libraries/aupt-lib/src/android/support/test/aupt/DataCollector.java
index bec2daa..127f5ff 100644
--- a/libraries/aupt-lib/src/android/support/test/aupt/DataCollector.java
+++ b/libraries/aupt-lib/src/android/support/test/aupt/DataCollector.java
@@ -17,333 +17,155 @@
 package android.support.test.aupt;
 
 import android.app.Instrumentation;
-import android.os.Environment;
-import android.os.ParcelFileDescriptor;
 import android.os.SystemClock;
 import android.util.Log;
 
-import java.io.ByteArrayOutputStream;
 import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
 import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.Locale;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 public class DataCollector {
     private static final String TAG = "AuptDataCollector";
-    private long mBugreportInterval, mMeminfoInterval, mCpuinfoInterval, mFragmentationInterval,
-            mIonHeapInterval, mPageTypeInfoInterval, mTraceInterval;
-    private File mResultsDirectory;
 
-    private Thread mLoggerThread;
-    private Logger mLogger;
-    private Instrumentation mInstrumentation;
+    private final AtomicBoolean mStopped = new AtomicBoolean(true);
+    private final Map<LogGenerator, Long> generatorsWithIntervals = new HashMap<>();
+    private final Map<LogGenerator, Long> mLastUpdate = new HashMap<>();
+    private final Instrumentation instrumentation;
+    private final String resultsDirectory;
+    private final long mSleepInterval;
 
-    public DataCollector(long bugreportInterval, long meminfoInterval, long cpuinfoInterval,
-            long fragmentationInterval, long ionHeapInterval, long pagetypeinfoInterval,
-            long traceInterval, File outputLocation, Instrumentation intrumentation) {
-        mBugreportInterval = bugreportInterval;
-        mMeminfoInterval = meminfoInterval;
-        mCpuinfoInterval = cpuinfoInterval;
-        mFragmentationInterval = fragmentationInterval;
-        mIonHeapInterval = ionHeapInterval;
-        mPageTypeInfoInterval = pagetypeinfoInterval;
-        mResultsDirectory = outputLocation;
-        mTraceInterval = traceInterval;
-        mInstrumentation = intrumentation;
-    }
+    private Thread mThread;
 
-    public void start() {
-        mLogger = new Logger();
-        mLoggerThread = new Thread(mLogger);
-        mLoggerThread.start();
-    }
-
-    public void stop() {
-        mLogger.stop();
-        try {
-            mLoggerThread.join();
-        } catch (InterruptedException e) {
-            // ignore
+    /**
+     * Add a generator iff the interval is valid (i.e. > 0).
+     */
+    private void put(LogGenerator key, Long interval) {
+        if (interval > 0) {
+            generatorsWithIntervals.put(key, interval);
         }
     }
 
-    private class Logger implements Runnable {
-        private final long mIntervals[] = {
-                mBugreportInterval, mMeminfoInterval, mCpuinfoInterval, mFragmentationInterval,
-                mIonHeapInterval, mPageTypeInfoInterval, mTraceInterval
-        };
-        private final LogGenerator mLoggers[] = {
-                new BugreportGenerator(), new CompactMemInfoGenerator(), new CpuInfoGenerator(),
-                new FragmentationGenerator(), new IonHeapGenerator(), new PageTypeInfoGenerator(),
-                new TraceGenerator()
-        };
+    public DataCollector(long bugreportInterval, long graphicsInterval,      long meminfoInterval,
+                         long cpuinfoInterval,   long fragmentationInterval, long ionHeapInterval,
+                         long pagetypeinfoInterval, long traceInterval,
+                         long bugreportzInterval, File outputLocation, Instrumentation instr) {
 
-        private final long mLastUpdate[] = new long[mLoggers.length];
-        private final long mSleepInterval;
+        resultsDirectory = outputLocation.getPath();
+        instrumentation = instr;
 
-        private boolean mStopped = false;
+        if (bugreportzInterval > 0) {
+            put(LogGenerator.BUGREPORTZ, bugreportzInterval);
+            if (bugreportInterval > 0) {
+                Log.w(TAG, String.format("Both zipped and flat bugreports are enabled. Defaulting"
+                        + " to use zipped bugreports, at %s ms interval.", bugreportzInterval));
+            }
+        } else if (bugreportInterval > 0) {
+            put(LogGenerator.BUGREPORT, bugreportInterval);
+        }
+        put(LogGenerator.CPU_INFO, cpuinfoInterval);
+        put(LogGenerator.FRAGMENTATION, fragmentationInterval);
+        put(LogGenerator.GRAPHICS_STATS, graphicsInterval);
+        put(LogGenerator.ION_HEAP, ionHeapInterval);
+        put(LogGenerator.MEM_INFO, meminfoInterval);
+        put(LogGenerator.PAGETYPE_INFO, pagetypeinfoInterval);
+        put(LogGenerator.TRACE, traceInterval);
 
-        public Logger() {
-            for (int i = 0; i < mIntervals.length; i++) {
-                if (mIntervals[i] > 0) {
-                    try {
-                        mLoggers[i].createLog();
-                    } catch (InterruptedException e) {
-                        // ignore
-                    }
-                    mLastUpdate[i] = SystemClock.uptimeMillis();
+        mSleepInterval = gcd(generatorsWithIntervals.values());
+    }
+
+    public synchronized void start() {
+        if (mStopped.getAndSet(false)) {
+            /* Initialize the LastUpdates to the current time */
+            for (Map.Entry<LogGenerator, Long> entry : generatorsWithIntervals.entrySet()) {
+                if (entry.getValue() > 0) {
+                    Log.d(TAG, "Collecting " + entry.getKey() + " logs every " +
+                        entry.getValue() + " milliseconds");
+
+                    mLastUpdate.put(entry.getKey(), SystemClock.uptimeMillis());
                 }
             }
 
-            mSleepInterval = gcd(mIntervals);
+            mThread = new Thread(new Runnable() {
+                @Override
+                public void run() {
+                    loop();
+                }
+            });
+            mThread.start();
+        } else {
+            Log.e(TAG, "Tried to start a started DataCollector!");
         }
+    }
 
-        public void stop() {
-            synchronized(this) {
-                mStopped = true;
-                notifyAll();
+    public synchronized void stop() {
+        if (!mStopped.getAndSet(true)) {
+            mThread.interrupt();
+
+            try {
+                mThread.join();
+            } catch (InterruptedException e) {
+                // ignore
             }
+        } else {
+            Log.e(TAG, "Tried to stop a stoppped DataCollector!");
+        }
+    }
+
+    private void loop() {
+        if (mSleepInterval <= 0) {
+            return;
         }
 
-        private long gcd(long values[]) {
-            if (values.length < 2)
-                return 0;
+        while (!mStopped.get()) {
+            try {
+                for (Map.Entry<LogGenerator, Long> entry : generatorsWithIntervals.entrySet()) {
+                    Long t = SystemClock.uptimeMillis() - mLastUpdate.get(entry.getKey());
 
-            long gcdSoFar = values[0];
-            for (int i = 1; i < values.length; i++) {
-                gcdSoFar = gcd(gcdSoFar, values[i]);
-            }
-
-            return gcdSoFar;
-        }
-
-        private long gcd(long a, long b) {
-            if (a == 0)
-                return b;
-            if (b == 0)
-                return a;
-            if (a > b)
-                return gcd(b, a % b);
-            else
-                return gcd(a, b % a);
-        }
-
-        @Override
-        public void run() {
-            if (mSleepInterval <= 0)
-                return;
-
-            synchronized(this) {
-                while (!mStopped) {
-                    try {
-                        for (int i = 0; i < mIntervals.length; i++) {
-                            if (mIntervals[i] > 0 &&
-                                    SystemClock.uptimeMillis() - mLastUpdate[i] > mIntervals[i]) {
-                                mLoggers[i].createLog();
-                                mLastUpdate[i] = SystemClock.uptimeMillis();
-                            }
+                    if (entry.getValue() > 0 && t >= entry.getValue()) {
+                        try {
+                            entry.getKey().save(instrumentation, resultsDirectory);
+                        } catch (IOException ex) {
+                            Log.e(TAG, "Error writing results in " + resultsDirectory +
+                                    ": " + ex.toString());
                         }
-                        wait(mSleepInterval);
-                    } catch (InterruptedException e) {
-                        // Ignore.
+
+                        mLastUpdate.put(entry.getKey(), SystemClock.uptimeMillis());
                     }
                 }
+
+                Thread.sleep(mSleepInterval);
+            } catch (InterruptedException e) {
+                // Ignore.
             }
         }
     }
 
-    private interface LogGenerator {
-        public void createLog() throws InterruptedException;
-    }
-
-    private class CompactMemInfoGenerator implements LogGenerator {
-        @Override
-        public void createLog() throws InterruptedException {
-            try {
-                saveCompactMeminfo(mResultsDirectory + "/compact-meminfo-%s.txt");
-            } catch (IOException ioe) {
-                Log.w(TAG, "Error while saving dumpsys meminfo -c: " + ioe.getMessage());
-            }
+    private long gcd(Collection<Long> values) {
+        if (values.size() < 1) {
+            return 0;
         }
-    }
 
-    private class CpuInfoGenerator implements LogGenerator {
-        @Override
-        public void createLog() throws InterruptedException {
-            try {
-                saveCpuinfo(mResultsDirectory + "/cpuinfo-%s.txt");
-            } catch (IOException ioe) {
-                Log.w(TAG, "Error while saving dumpsys cpuinfo : " + ioe.getMessage());
-            }
+        long gcdSoFar = values.iterator().next();
+
+        for (Long value : values) {
+            gcdSoFar = gcd(gcdSoFar, value);
         }
+
+        return gcdSoFar;
     }
 
-    private class BugreportGenerator implements LogGenerator {
-        @Override
-        public void createLog() throws InterruptedException {
-            try {
-                saveBugreport(mResultsDirectory + "/bugreport-%s.txt");
-            } catch (IOException e) {
-                Log.w(TAG, String.format("Failed to take bugreport: %s", e.getMessage()));
-            }
-        }
-    }
-
-    private class FragmentationGenerator implements LogGenerator {
-        @Override
-        public void createLog() throws InterruptedException {
-            try {
-                saveFragmentation(mResultsDirectory + "/unusable-index-%s.txt");
-            } catch (IOException e) {
-                Log.w(TAG, String.format("Failed to save buddyinfo: %s", e.getMessage()));
-            }
-        }
-    }
-
-    private class IonHeapGenerator implements LogGenerator {
-        @Override
-        public void createLog() throws InterruptedException {
-            try {
-                saveIonHeap("audio", mResultsDirectory + "/ion-audio-%s.txt");
-                saveIonHeap("system", mResultsDirectory + "/ion-system-%s.txt");
-            } catch (IOException e) {
-                Log.w(TAG, String.format("Failed to save ION heap: %s", e.getMessage()));
-            }
-        }
-    }
-
-    private class PageTypeInfoGenerator implements LogGenerator {
-        @Override
-        public void createLog() throws InterruptedException {
-            try {
-                savePageTypeInfo(mResultsDirectory + "/pagetypeinfo-%s.txt");
-            } catch (IOException e) {
-                Log.w(TAG, String.format("Failed to save pagetypeinfo: %s", e.getMessage()));
-            }
-        }
-    }
-
-    private class TraceGenerator implements LogGenerator {
-        @Override
-        public void createLog() throws InterruptedException {
-            try {
-                saveTrace(mResultsDirectory + "/trace-%s.txt");
-            } catch (IOException e) {
-                Log.w(TAG, String.format("Failed to save trace: %s", e.getMessage()));
-            }
-        }
-    }
-
-    public void saveCompactMeminfo(String filename)
-            throws FileNotFoundException, IOException, InterruptedException {
-        saveProcessOutput("dumpsys meminfo -c -S", filename);
-    }
-
-    public void saveCpuinfo(String filename)
-            throws FileNotFoundException, IOException, InterruptedException {
-        saveProcessOutput("dumpsys cpuinfo", filename);
-    }
-
-    public void saveFragmentation(String filename)
-            throws FileNotFoundException, IOException, InterruptedException {
-        saveProcessOutput("cat /d/extfrag/unusable_index", filename);
-    }
-
-    public void saveIonHeap(String type, String filename)
-            throws FileNotFoundException, IOException, InterruptedException {
-        saveProcessOutput(String.format("cat /d/ion/heaps/%s", type), filename);
-    }
-
-    public void savePageTypeInfo(String filename)
-            throws FileNotFoundException, IOException, InterruptedException {
-        saveProcessOutput("cat /proc/pagetypeinfo", filename);
-    }
-
-    public void saveTrace(String filename)
-            throws FileNotFoundException, IOException, InterruptedException {
-        saveProcessOutput("cat /sys/kernel/debug/tracing/trace", filename);
-    }
-
-    public void saveBugreport(String filename)
-            throws IOException, InterruptedException {
-        ByteArrayOutputStream baos = new ByteArrayOutputStream();
-        // Spaces matter in the following command line. Make sure there are no spaces
-        // in the filename and around the '>' sign.
-        String cmdline = String.format("/system/bin/sh -c /system/bin/bugreport>%s",
-                templateToFilename(filename));
-        saveProcessOutput(cmdline, baos);
-        baos.close();
-    }
-
-    public void dumpMeminfo(String notes) {
-        long epochSeconds = System.currentTimeMillis() / 1000;
-        File outputDir = new File(Environment.getExternalStorageDirectory(), "meminfo");
-        Log.i(TAG, outputDir.toString());
-        if (!outputDir.exists()) {
-            boolean yes  = outputDir.mkdirs();
-            Log.i(TAG, yes ? "created" : "not created");
-        }
-        File outputFile = new File(outputDir, String.format("%d.txt", epochSeconds));
-        Log.i(TAG, outputFile.toString());
-        FileOutputStream fos = null;
-
-        try {
-            fos = new FileOutputStream(outputFile);
-            fos.write(String.format("notes: %s\n\n", notes).getBytes());
-
-            saveProcessOutput("dumpsys meminfo -c", fos);
-            fos.close();
-        } catch (FileNotFoundException e) {
-            Log.e(TAG, "exception while dumping meminfo", e);
-        } catch (IOException e) {
-            Log.e(TAG, "exception while dumping meminfo", e);
-        }
-    }
-
-    private void saveProcessOutput(String command, String filenameTemplate)
-            throws IOException, FileNotFoundException {
-        String outFilename = templateToFilename(filenameTemplate);
-        File file = new File(outFilename);
-        Log.d(TAG, String.format("Saving command \"%s\" output into file %s",
-                command, file.getAbsolutePath()));
-
-        OutputStream out = new FileOutputStream(file);
-        saveProcessOutput(command, out);
-        out.close();
-    }
-
-    private String templateToFilename(String filenameTemplate) {
-        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US);
-        return String.format(filenameTemplate, sdf.format(new Date()));
-    }
-
-    public void saveProcessOutput(String command, OutputStream out) throws IOException {
-        InputStream in = null;
-        try {
-            ParcelFileDescriptor pfd =
-                    mInstrumentation.getUiAutomation().executeShellCommand(command);
-            in = new ParcelFileDescriptor.AutoCloseInputStream(pfd);
-            byte[] buffer = new byte[4096];  //4K buffer
-            int bytesRead = -1;
-            while (true) {
-                bytesRead = in.read(buffer);
-                if (bytesRead == -1) {
-                    break;
-                }
-                out.write(buffer, 0, bytesRead);
-            }
-        } finally {
-            if (in != null) {
-                in.close();
-            }
-            if (out != null) {
-                out.flush();
-            }
+    private long gcd(long a, long b) {
+        if (a == 0) {
+            return b;
+        } else if (b == 0) {
+            return a;
+        } else if (a > b) {
+            return gcd(b, a % b);
+        } else {
+            return gcd(a, b % a);
         }
     }
 }
diff --git a/libraries/aupt-lib/src/android/support/test/aupt/DexTestRunner.java b/libraries/aupt-lib/src/android/support/test/aupt/DexTestRunner.java
new file mode 100644
index 0000000..125abe8
--- /dev/null
+++ b/libraries/aupt-lib/src/android/support/test/aupt/DexTestRunner.java
@@ -0,0 +1,356 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.test.aupt;
+
+import android.app.Instrumentation;
+import android.test.AndroidTestRunner;
+import android.util.Log;
+
+import dalvik.system.DexClassLoader;
+
+import junit.framework.AssertionFailedError;
+import junit.framework.Test;
+import junit.framework.TestCase;
+import junit.framework.TestListener;
+import junit.framework.TestResult;
+import junit.framework.TestSuite;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Enumeration;
+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.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * A DexTestRunner runs tests by name from a given list of JARs,
+ * with the following additional magic:
+ *
+ * - Custom ClassLoading from given dexed Jars
+ * - Custom test scheduling (via Scheduler)
+ *
+ * In addition to the parameters in the constructor, be sure to run setTest or setTestClassName
+ * before attempting to runTest.
+ */
+class DexTestRunner extends AndroidTestRunner {
+    private static final String LOG_TAG = DexTestRunner.class.getSimpleName();
+
+    /* Constants */
+    static final String DEFAULT_JAR_PATH = "/data/local/tmp/";
+    static final String DEX_OPT_PATH = "dex-test-opt";
+
+    /* Private fields */
+    private final List<TestListener> mTestListeners = new ArrayList<>();
+    private final DexClassLoader mLoader;
+    private final long mTestTimeoutMillis;
+    private final long mSuiteTimeoutMillis;
+
+    /* TestRunner State */
+    protected TestResult mTestResult = new TestResult();
+    protected List<TestCase> mTestCases = new ArrayList<>();
+    protected String mTestClassName;
+    protected Instrumentation mInstrumentation;
+    protected Scheduler mScheduler;
+    protected long mSuiteEndTime;
+
+    /** A temporary ExecutorService to manage running the current test. */
+    private ExecutorService mExecutorService;
+
+    /** The current test. */
+    private TestCase mTestCase;
+
+    /* Field initialization */
+    DexTestRunner(
+            Instrumentation instrumentation,
+            Scheduler scheduler,
+            List<String> jars,
+            long testTimeoutMillis,
+            long suiteTimeoutMillis) {
+        super();
+
+        mInstrumentation = instrumentation;
+        mScheduler = scheduler;
+        mLoader = makeLoader(jars);
+        mTestTimeoutMillis = testTimeoutMillis;
+        mSuiteTimeoutMillis = suiteTimeoutMillis;
+    }
+
+    /* Main methods */
+
+    @Override
+    public void runTest() {
+        runTest(newResult());
+    }
+
+    @Override
+    public synchronized void runTest(final TestResult testResult) {
+        mTestResult = testResult;
+        mSuiteEndTime = System.currentTimeMillis() + mSuiteTimeoutMillis;
+
+        for (final TestCase testCase : mScheduler.apply(mTestCases)) {
+            // Timeout the suite if we've passed the end time.
+            if (mSuiteTimeoutMillis != 0 && System.currentTimeMillis() > mSuiteEndTime) {
+                Log.w(LOG_TAG, String.format("Ending suite after %d mins running.",
+                        TimeUnit.MILLISECONDS.toMinutes(mSuiteTimeoutMillis)));
+                break;
+            }
+
+            mExecutorService = Executors.newSingleThreadExecutor();
+            mTestCase = testCase;
+
+            // A Future that calls testCase::run. The reasoning behind using a thread here
+            // is that AuptTestRunner should be able to interrupt it (via killTest) if it runs
+            // too long; and interrupting the main thread here without actually exiting is tricky.
+            Future<TestResult> result =
+                    mExecutorService.submit(
+                            new Callable<TestResult>() {
+                                @Override
+                                public TestResult call() throws Exception {
+                                    testCase.run(testResult);
+                                    return testResult;
+                                }
+                            });
+
+            try {
+                // Run our test-running thread and wait on it.
+                result.get(mTestTimeoutMillis, TimeUnit.MILLISECONDS);
+            } catch (TimeoutException e) {
+                killTest(e);
+            } catch (ExecutionException e) {
+                onError(testCase, e.getCause());
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+            } finally {
+                mExecutorService.shutdownNow();
+                mTestCase = null;
+            }
+        }
+    }
+
+    /** Interrupt the current test with the given exception. */
+    void killTest(Exception e) {
+        if (mTestCase != null) {
+            // First, tell our listeners.
+            onError(mTestCase, e);
+
+            // Kill the test.
+            mExecutorService.shutdownNow();
+        }
+    }
+
+    /* TestCase Initialization */
+
+    @Override
+    public void setTestClassName(String className, String methodName) {
+        mTestCases.clear();
+        addTestClassByName(className, methodName);
+    }
+
+    void addTestClassByName(final String className, final String methodName) {
+        try {
+            final Class<?> testClass = mLoader.loadClass(className);
+
+            if (Test.class.isAssignableFrom(testClass)) {
+                Test test = null;
+
+                try {
+                    // Make sure it works
+                    test = (Test) testClass.getConstructor().newInstance();
+                } catch (Exception e1) { /* If we fail, test will just stay null */ }
+
+                try {
+                    test = (Test) testClass.getConstructor(String.class).newInstance(methodName);
+                } catch (Exception e2) { /* If we fail, test will just stay null */ }
+
+                addTest(test);
+            } else {
+                throw new RuntimeException("Test class not found: " + className);
+            }
+        } catch (ClassNotFoundException ex) {
+            throw new RuntimeException("Class not found: " + ex.getMessage());
+        }
+
+        if (mTestCases.isEmpty()) {
+            throw new RuntimeException("No tests found in " + className + "#" + methodName);
+        }
+    }
+
+    @Override
+    public void setTest(Test test) {
+        mTestCases.clear();
+        addTest(test);
+
+        // Update our test class name.
+        if (TestSuite.class.isAssignableFrom(test.getClass())) {
+            mTestClassName = ((TestSuite) test).getName();
+        } else if (TestCase.class.isAssignableFrom(test.getClass())) {
+            mTestClassName = ((TestCase) test).getName();
+        } else {
+            mTestClassName = test.getClass().getSimpleName();
+        }
+    }
+
+    public void addTest(Test test) {
+        if (test instanceof TestCase) {
+
+            mTestCases.add((TestCase) test);
+
+        } else if (test instanceof TestSuite) {
+            Enumeration<Test> tests = ((TestSuite) test).tests();
+
+            while (tests.hasMoreElements()) {
+                addTest(tests.nextElement());
+            }
+        } else {
+            throw new RuntimeException("Tried to add invalid test: " + test.toString());
+        }
+    }
+
+    /* State Manipulation Methods */
+
+    @Override
+    public void clearTestListeners() {
+        mTestListeners.clear();
+    }
+
+    @Override
+    public void addTestListener(TestListener testListener) {
+        if (testListener != null) {
+            mTestListeners.add(testListener);
+            mTestResult.addListener(testListener);
+        }
+    }
+
+    void addTestListenerIf(Boolean cond, TestListener testListener) {
+        if (cond && testListener != null) {
+            mTestListeners.add(testListener);
+        }
+    }
+
+    @Override
+    public List<TestCase> getTestCases() {
+        return mTestCases;
+    }
+
+    @Override
+    public void setInstrumentation(Instrumentation instrumentation) {
+        mInstrumentation = instrumentation;
+    }
+
+    @Override
+    public TestResult getTestResult() {
+        return mTestResult;
+    }
+
+    @Override
+    protected TestResult createTestResult() {
+        return new TestResult();
+    }
+
+    @Override
+    public String getTestClassName() {
+        return mTestClassName;
+    }
+
+    /* Listener Exception Callback. */
+
+    void onError(Test test, Throwable t) {
+        if (t instanceof AssertionFailedError) {
+            for (TestListener listener : mTestListeners) {
+                listener.addFailure(test, (AssertionFailedError) t);
+            }
+        } else {
+            for (TestListener listener : mTestListeners) {
+                listener.addError(test, t);
+            }
+        }
+    }
+
+    /* Package-private Utilities */
+
+    TestResult newResult() {
+        TestResult result = new TestResult();
+
+        for (TestListener listener: mTestListeners) {
+            result.addListener(listener);
+        }
+
+        return result;
+    }
+
+    static List<String> parseDexedJarPaths(String jarString) {
+        List<String> jars = new ArrayList<>();
+
+        for (String jar : jarString.split(":")) {
+            // Check that jar isn't empty, but don't fail because String::split will yield
+            // spurious empty results if, for example, we don't specify any jars, accidentally
+            // start with a leading colon, etc.
+            if (!jar.trim().isEmpty()) {
+                File jarFile = jar.startsWith("/")
+                        ? new File(jar)
+                        : new File(DEFAULT_JAR_PATH + jar);
+
+                if (jarFile.exists()) {
+                    jars.add(jarFile.getAbsolutePath());
+                } else {
+                    throw new RuntimeException("Can't find jar file " + jarFile);
+                }
+            }
+        }
+
+        return jars;
+    }
+
+    DexClassLoader getDexClassLoader() {
+        return mLoader;
+    }
+
+    DexClassLoader makeLoader(List<String> jars) {
+        StringBuilder jarFiles = new StringBuilder();
+
+        for (String jar : jars) {
+            if (new File(jar).exists() && new File(jar).canRead()) {
+                if (jarFiles.length() != 0) {
+                    jarFiles.append(File.pathSeparator);
+                }
+
+                jarFiles.append(jar);
+            } else {
+                throw new IllegalArgumentException(
+                        "Jar file does not exist or not accessible: "  + jar);
+            }
+        }
+
+        File optDir = new File(mInstrumentation.getTargetContext().getCacheDir(), DEX_OPT_PATH);
+
+        if (optDir.exists() || optDir.mkdirs()) {
+            return new DexClassLoader(
+                    jarFiles.toString(),
+                    optDir.getAbsolutePath(),
+                    null,
+                    DexTestRunner.class.getClassLoader());
+        } else {
+            throw new RuntimeException(
+                    "Failed to create dex optimization directory: " + optDir.getAbsolutePath());
+        }
+    }
+}
diff --git a/libraries/aupt-lib/src/android/support/test/aupt/FilesystemUtil.java b/libraries/aupt-lib/src/android/support/test/aupt/FilesystemUtil.java
new file mode 100644
index 0000000..0780000
--- /dev/null
+++ b/libraries/aupt-lib/src/android/support/test/aupt/FilesystemUtil.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.test.aupt;
+
+import android.app.ActivityManager;
+import android.app.Instrumentation;
+import android.os.Environment;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+public class FilesystemUtil {
+    private static final String TAG = FilesystemUtil.class.getSimpleName();
+
+    /** Save the output of a process to a file */
+    public static void saveProcessOutput(Instrumentation instr, String command, File file)
+            throws IOException {
+        Log.d(TAG, String.format("Saving command \"%s\" output into file %s",
+                command, file.getAbsolutePath()));
+
+        OutputStream out = new FileOutputStream(file);
+        saveProcessOutput(instr, command, out);
+        out.close();
+    }
+
+    /** Send the output of a process to an OutputStream. */
+    public static void saveProcessOutput(Instrumentation instr, String command, OutputStream out)
+            throws IOException {
+        try {
+            // First, try to execute via our UiAutomation
+            ParcelFileDescriptor pfd = instr.getUiAutomation().executeShellCommand(command);
+            pipe(new ParcelFileDescriptor.AutoCloseInputStream(pfd), out);
+        } catch (IllegalStateException ise) {
+            // If we don't have a UiAutomation, we'll get an IllegalStatException;
+            // so try to do it via an exec()
+            Process process = Runtime.getRuntime().exec(command);
+            pipe(process.getInputStream(), out);
+
+            // Wait for our process to finish
+            try {
+                process.waitFor();
+            } catch (InterruptedException ie) {
+                throw new IOException("Thread interrupted waiting for command: " + command);
+            }
+
+            // Make sure it succeeded.
+            if (process.exitValue() != 0) {
+                throw new IOException("Failed to save output of command: " + command);
+            }
+        }
+    }
+
+    /** Save a bugreport to the given file */
+    public static void saveBugreport(Instrumentation instr, String filename)
+            throws IOException, InterruptedException {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        String cmdline = String.format("/system/bin/sh -c /system/bin/bugreport>%s",
+                templateToFilename(filename));
+        saveProcessOutput(instr, cmdline, baos);
+        baos.close();
+    }
+
+    /** Save a bugreport to the given file */
+    public static void saveBugreportz(Instrumentation instr) throws IOException {
+        try {
+            ActivityManager.getService().requestBugReport(ActivityManager.BUGREPORT_OPTION_FULL);
+        } catch (RemoteException e) {
+            throw new IOException("Could not capture bugreportz", e);
+        }
+    }
+
+    /** Save annotated Meminfo to our default logging directory */
+    public static void dumpMeminfo(Instrumentation instr, String notes) {
+        long epochSeconds = System.currentTimeMillis() / 1000;
+        File outputDir = new File(Environment.getExternalStorageDirectory(), "meminfo");
+        Log.i(TAG, outputDir.toString());
+        if (!outputDir.exists()) {
+            boolean yes  = outputDir.mkdirs();
+            Log.i(TAG, yes ? "created" : "not created");
+        }
+        File outputFile = new File(outputDir, String.format("%d.txt", epochSeconds));
+        Log.i(TAG, outputFile.toString());
+        FileOutputStream fos = null;
+
+        try {
+            fos = new FileOutputStream(outputFile);
+            fos.write(String.format("notes: %s\n\n", notes).getBytes());
+
+            saveProcessOutput(instr, "dumpsys meminfo -c", fos);
+            fos.close();
+        } catch (FileNotFoundException e) {
+            Log.e(TAG, "exception while dumping meminfo", e);
+        } catch (IOException e) {
+            Log.e(TAG, "exception while dumping meminfo", e);
+        }
+    }
+
+    /** Splice the date into the "%s" in a file name */
+    public static String templateToFilename(String filenameTemplate) {
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US);
+        return String.format(filenameTemplate, sdf.format(new Date()));
+    }
+
+    /** Pipe an inputstream to an outputstream. This matches Apache's IOUtils::copy */
+    private static void pipe(InputStream in, OutputStream out) throws IOException {
+        byte[] buffer = new byte[4096];
+        int bytesRead = 0;
+
+        try {
+            while (bytesRead >= 0) {
+                out.write(buffer, 0, bytesRead);
+                bytesRead = in.read(buffer);
+            }
+        } finally {
+            in.close();
+            out.flush();
+        }
+    }
+}
diff --git a/libraries/aupt-lib/src/android/support/test/aupt/GraphicsStatsMonitor.java b/libraries/aupt-lib/src/android/support/test/aupt/GraphicsStatsMonitor.java
deleted file mode 100644
index 2efeace..0000000
--- a/libraries/aupt-lib/src/android/support/test/aupt/GraphicsStatsMonitor.java
+++ /dev/null
@@ -1,344 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.test.aupt;
-
-import android.app.UiAutomation;
-import android.os.Bundle;
-import android.os.ParcelFileDescriptor;
-import android.util.Log;
-
-import java.io.BufferedReader;
-import java.io.InputStreamReader;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Timer;
-import java.util.TimerTask;
-
-/**
- * GraphicsStatsMonitor is an internal monitor for AUPT to poll and track the information coming out
- * of the shell command, "dumpsys graphicsstats." In particular, the purpose of this is to see jank
- * statistics across the lengthy duration of an AUPT run.
- * <p>
- * To use the monitor, simply specify the options: trackJank true and jankInterval n, where n is
- * the polling interval in milliseconds. The default is 5 minutes. Also, it should be noted that
- * the trackJank option is unnecessary and this comment should be removed at the same time as it.
- * <p>
- * This graphics service monitors jank levels grouped by foreground process. Even when the process
- * is killed, the monitor will continue to track information, unless the buffer runs out of space.
- * This should only occur when too many foreground processes have been killed and the service
- * decides to clear itself. When pulling the information out of the monitor, these separate images
- * are combined to provide a single image as output. The linear information is preserved by simply
- * adding the values together. However, certain information such as the jank percentiles are
- * approximated using a weighted average.
- */
-public class GraphicsStatsMonitor {
-    private static final String TAG = "GraphicsStatsMonitor";
-
-    public static final int MS_IN_SECS = 1000;
-    public static final int SECS_IN_MIN = 60;
-    public static final long DEFAULT_INTERVAL_RATE = 5 * SECS_IN_MIN * MS_IN_SECS;
-
-    private Timer mIntervalTimer;
-    private TimerTask mIntervalTask;
-    private long mIntervalRate;
-    private boolean mIsRunning;
-
-    private Map<String, List<JankStat>> mGraphicsStatsRecords;
-
-
-    public GraphicsStatsMonitor () {
-        mIntervalTask = new TimerTask() {
-            @Override
-            public void run () {
-                if (mIsRunning) {
-                    grabStatsImage();
-                }
-            }
-        };
-        mIntervalRate = DEFAULT_INTERVAL_RATE;
-        mIsRunning = false;
-    }
-
-    /**
-     * Sets the monitoring interval rate if the monitor isn't currently running
-     */
-    public void setIntervalRate (long intervalRate) {
-        if (mIsRunning) {
-            Log.e(TAG, "Can't set interval rate for monitor that is already running");
-        } else if (intervalRate > 0L) {
-            mIntervalRate = intervalRate;
-            Log.v(TAG, String.format("Set jank monitor interval rate to %d", intervalRate));
-        }
-    }
-
-    /**
-     * Starts to monitor graphics stats on the interval timer after clearing the stats
-     */
-    public void startMonitoring () {
-        if (mGraphicsStatsRecords == null) {
-            mGraphicsStatsRecords = new HashMap<>();
-        }
-
-        clearGraphicsStats();
-
-        // Schedule a daemon timer to grab stats periodically
-        mIntervalTimer = new Timer(true);
-        mIntervalTimer.schedule(mIntervalTask, 0, mIntervalRate);
-
-        mIsRunning = true;
-        Log.d(TAG, "Started monitoring graphics stats");
-    }
-
-    /**
-     * Stops monitoring graphics stats by canceling the interval timer
-     */
-    public void stopMonitoring () {
-        mIntervalTimer.cancel();
-
-        mIsRunning = false;
-        Log.d(TAG, "Stopped monitoring graphics stats");
-    }
-
-    /**
-     * Takes a snapshot of the graphics stats and incorporates them into the process stats history
-     */
-    public void grabStatsImage () {
-        Log.v(TAG, "Grabbing image of graphics stats");
-        List<JankStat> allStats = gatherGraphicsStats();
-
-        for (JankStat procStats : allStats) {
-            List<JankStat> history;
-            if (mGraphicsStatsRecords.containsKey(procStats.packageName)) {
-                history = mGraphicsStatsRecords.get(procStats.packageName);
-                // Has the process been killed and restarted?
-                if (procStats.isContinuedFrom(history.get(history.size() - 1))) {
-                    // Process hasn't been killed and restarted; put the data
-                    history.set(history.size() - 1, procStats);
-                    Log.v(TAG, String.format("Process %s stats have not changed, overwriting data.",
-                            procStats.packageName));
-                } else {
-                    // Process has been killed and restarted; append the data
-                    history.add(procStats);
-                    Log.v(TAG, String.format("Process %s stats were restarted, appending data.",
-                            procStats.packageName));
-                }
-            } else {
-                // Initialize the process stats history list
-                history = new ArrayList<>();
-                history.add(procStats);
-                // Put the history list in the JankStats map
-                mGraphicsStatsRecords.put(procStats.packageName, history);
-                Log.v(TAG, String.format("New process, %s. Creating jank history.",
-                        procStats.packageName));
-            }
-        }
-    }
-
-    /**
-     * Aggregates the graphics stats for each process over its history. Merging specifications can
-     * be found in the static method {@link JankStat#mergeStatHistory}.
-     */
-    public List<JankStat> aggregateStatsImages () {
-        Log.d(TAG, "Aggregating graphics stats history");
-        List<JankStat> mergedStatsList = new ArrayList<JankStat>();
-
-        for (Map.Entry<String, List<JankStat>> record : mGraphicsStatsRecords.entrySet()) {
-            String proc = record.getKey();
-            List<JankStat> history = record.getValue();
-
-            Log.v(TAG, String.format("Aggregating stats for %s (%d set%s)", proc, history.size(),
-                    (history.size() > 1 ? "s" : "")));
-
-            JankStat mergedStats = JankStat.mergeStatHistory(history);
-            mergedStatsList.add(mergedStats);
-        }
-
-        return mergedStatsList;
-    }
-
-    /**
-     * Clears all graphics stats history data for all processes
-     */
-    public void clearStatsImages () {
-        mGraphicsStatsRecords.clear();
-    }
-
-    /**
-     * Resets graphics stats for all currently tracked processes
-     */
-    public void clearGraphicsStats () {
-        Log.d(TAG, "Reset all graphics stats");
-        List<JankStat> existingStats = gatherGraphicsStats();
-        for (JankStat stat : existingStats) {
-            executeShellCommand(String.format("dumpsys gfxinfo %s reset", stat.packageName));
-            Log.v(TAG, String.format("Cleared graphics stats for %s", stat.packageName));
-        }
-    }
-
-    /*
-     * Return JankStat objects from a stream representing the output of `dumpsys graphicsstats`
-     *
-     * This is broken out from gatherGraphicsStats for testing purposes
-     */
-    private List<JankStat> parseGraphicsStatsFromStream(BufferedReader stream) throws IOException {
-        // TODO: this kind of stream filtering is much nicer using the Java 8 functional
-        // primitives. Once AUPT goes to jdk8, we should refactor this.
-
-        JankStat.StatPattern patterns[] = JankStat.StatPattern.values();
-        List<JankStat> result = new ArrayList<>();
-        JankStat nextStat = new JankStat(null, 0L, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
-        String line;
-
-        // Split the stream by package
-        while((line = stream.readLine()) != null) {
-            if(JankStat.StatPattern.PACKAGE.parse(line) != null) {
-
-                // When the output of `dumpsys graphicsstats` enters a new set of jank stats, we start
-                // with a line matching the PACKAGE pattern; so we have to make a new JankStat for it and
-                // save the old one.
-
-                if(nextStat.packageName != null) {
-                    Log.v(TAG, String.format("Gathered jank info from process %s.", nextStat.packageName));
-                    result.add(nextStat);
-                }
-
-                nextStat = new JankStat(JankStat.StatPattern.PACKAGE.parse(line),
-                        0L, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
-
-            } else {
-
-                // NOTE: we know these theoretically come in order, so we don't have to iterate
-                // through the whole pattern array every time; but there is enough variation in
-                // the code generating these log lines to justify a more input-robust parser
-
-                for (JankStat.StatPattern p : patterns) {
-                    if (p.getPattern() != null && p.parse(line) != null) {
-                        switch (p) {
-                            case STATS_SINCE:
-                                nextStat.statsSince = Long.parseLong(p.parse(line));
-                                break;
-                            case TOTAL_FRAMES:
-                                nextStat.totalFrames = Integer.valueOf(p.parse(line));
-                                break;
-                            case NUM_JANKY:
-                                nextStat.jankyFrames = Integer.valueOf(p.parse(line));
-                                break;
-                            case FRAME_TIME_50TH:
-                                nextStat.frameTime50th = Integer.valueOf(p.parse(line));
-                                break;
-                            case FRAME_TIME_90TH:
-                                nextStat.frameTime90th = Integer.valueOf(p.parse(line));
-                                break;
-                            case FRAME_TIME_95TH:
-                                nextStat.frameTime95th = Integer.valueOf(p.parse(line));
-                                break;
-                            case FRAME_TIME_99TH:
-                                nextStat.frameTime99th = Integer.valueOf(p.parse(line));
-                                break;
-                            case NUM_MISSED_VSYNC:
-                                nextStat.numMissedVsync = Integer.valueOf(p.parse(line));
-                                break;
-                            case NUM_HIGH_INPUT_LATENCY:
-                                nextStat.numHighLatency = Integer.valueOf(p.parse(line));
-                                break;
-                            case NUM_SLOW_UI_THREAD:
-                                nextStat.numSlowUiThread = Integer.valueOf(p.parse(line));
-                                break;
-                            case NUM_SLOW_BITMAP_UPLOADS:
-                                nextStat.numSlowBitmap = Integer.valueOf(p.parse(line));
-                                break;
-                            case NUM_SLOW_DRAW:
-                                nextStat.numSlowDraw = Integer.valueOf(p.parse(line));
-                                break;
-                            default:
-                                throw new RuntimeException(
-                                        "Unexpected parsing state in GraphicsStateMonitor");
-                        }
-                    }
-                }
-            }
-        }
-
-        // Remember to add the last JankStat
-        // We can't wrap this in the previous call because BufferedReader doesn't have a .peek()
-        if(nextStat.packageName != null) {
-            Log.v(TAG, String.format("Gathered jank info from process %s.", nextStat.packageName));
-            result.add(nextStat);
-        }
-
-        return result;
-    }
-
-    /**
-     * Return JankStat objects with metric data for all currently tracked processes
-     */
-    public List<JankStat> gatherGraphicsStats () {
-        Log.v(TAG, "Gather all graphics stats");
-        BufferedReader stream = executeShellCommand("dumpsys graphicsstats");
-
-        try {
-            return parseGraphicsStatsFromStream(stream);
-        } catch (IOException exception) {
-            Log.e(TAG, "Error with buffered reader", exception);
-            return null;
-        } finally {
-            try {
-                if (stream != null) {
-                    stream.close();
-                }
-            } catch (IOException exception) {
-                Log.e(TAG, "Error with closing the stream", exception);
-            }
-        }
-    }
-
-    /**
-     * UiAutomation is included solely for the purpose of executing shell commands
-     */
-    private UiAutomation mUiAutomation;
-
-    /**
-     * Executes a shell command through UiAutomation and puts the results in an
-     * InputStreamReader that is returned inside a BufferedReader.
-     * @param command the command to be executed in the adb shell
-     * @result a BufferedReader that reads the command output
-     */
-    public BufferedReader executeShellCommand (String command) {
-        ParcelFileDescriptor stdout = getUiAutomation().executeShellCommand(command);
-
-        BufferedReader stream = new BufferedReader(new InputStreamReader(
-                new ParcelFileDescriptor.AutoCloseInputStream(stdout)));
-        return stream;
-    }
-
-    /**
-     * Sets the UiAutomation member for shell execution
-     */
-    public void setUiAutomation (UiAutomation uiAutomation) {
-        mUiAutomation = uiAutomation;
-    }
-
-    /**
-     * @return UiAutomation instance from Aupt instrumentation
-     */
-    public UiAutomation getUiAutomation () {
-        return mUiAutomation;
-    }
-}
diff --git a/libraries/aupt-lib/src/android/support/test/aupt/JankStat.java b/libraries/aupt-lib/src/android/support/test/aupt/JankStat.java
deleted file mode 100644
index d65e102..0000000
--- a/libraries/aupt-lib/src/android/support/test/aupt/JankStat.java
+++ /dev/null
@@ -1,273 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.support.test.aupt;
-
-import android.util.Log;
-
-import java.lang.StringBuilder;
-import java.util.List;
-import java.util.regex.Pattern;
-import java.util.regex.Matcher;
-
-import org.json.JSONObject;
-import org.json.JSONException;
-
-/**
- * This class is like a C-style struct that holds individual process information from the
- * dumpsys graphicsstats command. It also includes an enumeration, originally from the
- * JankTestHelper code, which pattern matches against the dump data to find the relevant
- * information.
- */
-public class JankStat {
-    private static final String TAG = "JankStat";
-
-    // Patterns used for parsing dumpsys graphicsstats
-    public enum StatPattern {
-        PACKAGE("package",
-                Pattern.compile("\\s*Package: (.*)"), 1),
-
-        STATS_SINCE("startTime",
-                Pattern.compile("\\s*Stats since: (\\d+)ns"), 1),
-
-        TOTAL_FRAMES("frameCount",
-                Pattern.compile("\\s*Total frames rendered: (\\d+)"), 1),
-
-        NUM_JANKY("jankyCount",
-                Pattern.compile("\\s*Janky frames: (\\d+) (.*)"), 1),
-
-        FRAME_TIME_50TH("percentile50",
-                Pattern.compile("\\s*50th percentile: (\\d+)ms"), 1),
-
-        FRAME_TIME_90TH("percentile90",
-                Pattern.compile("\\s*90th percentile: (\\d+)ms"), 1),
-
-        FRAME_TIME_95TH("percentile95",
-                Pattern.compile("\\s*95th percentile: (\\d+)ms"), 1),
-
-        FRAME_TIME_99TH("percentile99",
-                Pattern.compile("\\s*99th percentile: (\\d+)ms"), 1),
-
-        NUM_MISSED_VSYNC("missedVsyncCount",
-                Pattern.compile("\\s*Number Missed Vsync: (\\d+)"), 1),
-
-        NUM_HIGH_INPUT_LATENCY("highLatencyCount",
-                Pattern.compile("\\s*Number High input latency: (\\d+)"), 1),
-
-        NUM_SLOW_UI_THREAD("slowUIThreadCount",
-                Pattern.compile("\\s*Number Slow UI thread: (\\d+)"), 1),
-
-        NUM_SLOW_BITMAP_UPLOADS("slowBitmapUploadCount",
-                Pattern.compile("\\s*Number Slow bitmap uploads: (\\d+)"), 1),
-
-        NUM_SLOW_DRAW("slowDrawCmdCount",
-                Pattern.compile("\\s*Number Slow issue draw commands: (\\d+)"), 1),
-
-        AGGREGATE_COUNT("aggregateCount", null, 1);
-
-        private String mName;
-        private Pattern mParsePattern;
-        private int mGroupIdx;
-
-        /**
-         * Constructs each pattern for parsing the statistics
-         * generated by `dumpsys graphicsstats`
-         *
-         * "name" is a unique JSON key for the field
-         * "pattern" is the regex for parsing out the field
-         * "idx" the match-index for the relevant field in that pattern
-         */
-        StatPattern(String name, Pattern pattern, int idx) {
-            mName = name;
-            mParsePattern = pattern;
-            mGroupIdx = idx;
-        }
-
-        String parse(String line) {
-            String ret = null;
-            Matcher matcher = mParsePattern.matcher(line);
-            if (matcher.matches()) {
-                ret = matcher.group(mGroupIdx);
-            }
-            return ret;
-        }
-
-        Pattern getPattern() {
-            return mParsePattern;
-        }
-
-        String getName() {
-            return mName;
-        }
-    }
-
-    public String packageName;
-    public Long statsSince;
-    public Integer totalFrames;
-    public Integer jankyFrames;
-    public Integer frameTime50th;
-    public Integer frameTime90th;
-    public Integer frameTime95th;
-    public Integer frameTime99th;
-    public Integer numMissedVsync;
-    public Integer numHighLatency;
-    public Integer numSlowUiThread;
-    public Integer numSlowBitmap;
-    public Integer numSlowDraw;
-    public Integer aggregateCount;
-
-    public JankStat (String pkg, long since, int total, int janky, int ft50, int ft90, int ft95,
-            int ft99, int vsync, int latency, int slowUi, int slowBmp, int slowDraw,
-            int aggCount) {
-        packageName = pkg;
-        statsSince = since;
-        totalFrames = total;
-        jankyFrames = janky;
-        frameTime50th = ft50;
-        frameTime90th = ft90;
-        frameTime95th = ft95;
-        frameTime99th = ft99;
-        numMissedVsync = vsync;
-        numHighLatency = latency;
-        numSlowUiThread = slowUi;
-        numSlowBitmap = slowBmp;
-        numSlowDraw = slowDraw;
-        aggregateCount = aggCount;
-    }
-
-    /**
-     * Determines if this set of janks stats is aggregated from the
-     * previous set of metrics or if they are a new set, meaning the
-     * old process was killed, had its stats reset, and was then
-     * restarted.
-     */
-    public boolean isContinuedFrom (JankStat prevMetrics) {
-        return statsSince == prevMetrics.statsSince;
-    }
-
-    /**
-     * Returns the percent of frames that appeared janky
-     */
-    public float getPercentJankyFrames () {
-        return jankyFrames / (float)totalFrames;
-    }
-
-    /**
-     * Serialize this object into a JSONObject
-     */
-    public JSONObject toJson () throws JSONException {
-        return new JSONObject().
-                put(StatPattern.PACKAGE.getName(), packageName).
-                put(StatPattern.STATS_SINCE.getName(), statsSince).
-                put(StatPattern.TOTAL_FRAMES.getName(), totalFrames).
-                put(StatPattern.NUM_JANKY.getName(), jankyFrames).
-                put(StatPattern.FRAME_TIME_50TH.getName(), frameTime50th).
-                put(StatPattern.FRAME_TIME_90TH.getName(), frameTime90th).
-                put(StatPattern.FRAME_TIME_95TH.getName(), frameTime95th).
-                put(StatPattern.FRAME_TIME_99TH.getName(), frameTime99th).
-                put(StatPattern.NUM_MISSED_VSYNC.getName(), numMissedVsync).
-                put(StatPattern.NUM_HIGH_INPUT_LATENCY.getName(), numHighLatency).
-                put(StatPattern.NUM_SLOW_UI_THREAD.getName(), numSlowUiThread).
-                put(StatPattern.NUM_SLOW_BITMAP_UPLOADS.getName(), numSlowBitmap).
-                put(StatPattern.NUM_SLOW_DRAW.getName(), numSlowDraw).
-                put(StatPattern.AGGREGATE_COUNT.getName(), aggregateCount);
-    }
-
-    /**
-     * @{inheritDoc}
-     */
-    @Override
-    public String toString () {
-      try {
-        return toJson().toString(4);
-      } catch (JSONException e) {
-        throw new RuntimeException("Error serializing JankStat: " + e.toString());
-      }
-    }
-
-    /**
-     * Merges the stat history of a sequence of stats.
-     *
-     * Final count value = sum of count values across stats
-     * Final ##th percentile = weighted average of ##th, weight by total frames
-     *     ## = 90, 95, and 99
-     */
-    public static JankStat mergeStatHistory (List<JankStat> statHistory) {
-        if (statHistory.size() == 0)
-            return null;
-        else if (statHistory.size() == 1)
-            return statHistory.get(0);
-
-        String pkg = statHistory.get(0).packageName;
-        long totalStatsSince = statHistory.get(0).statsSince;
-        int totalTotalFrames = 0;
-        int totalJankyFrames = 0;
-        int totalNumMissedVsync = 0;
-        int totalNumHighLatency = 0;
-        int totalNumSlowUiThread = 0;
-        int totalNumSlowBitmap = 0;
-        int totalNumSlowDraw = 0;
-
-        for (JankStat stat : statHistory) {
-            totalTotalFrames += stat.totalFrames;
-            totalJankyFrames += stat.jankyFrames;
-            totalNumMissedVsync += stat.numMissedVsync;
-            totalNumHighLatency += stat.numHighLatency;
-            totalNumSlowUiThread += stat.numSlowUiThread;
-            totalNumSlowBitmap += stat.numSlowBitmap;
-            totalNumSlowDraw += stat.numSlowDraw;
-        }
-
-        float wgtAvgPercentile50 = 0f;
-        float wgtAvgPercentile90 = 0f;
-        float wgtAvgPercentile95 = 0f;
-        float wgtAvgPercentile99 = 0f;
-
-        for (JankStat stat : statHistory) {
-            float weight = ((float)stat.totalFrames / totalTotalFrames);
-            Log.v(TAG, String.format("Calculated weight is %f", weight));
-            wgtAvgPercentile50 += stat.frameTime50th * weight;
-            wgtAvgPercentile90 += stat.frameTime90th * weight;
-            wgtAvgPercentile95 += stat.frameTime95th * weight;
-            wgtAvgPercentile99 += stat.frameTime99th * weight;
-        }
-
-        int perc50 = (int)Math.ceil(wgtAvgPercentile50);
-        int perc90 = (int)Math.ceil(wgtAvgPercentile90);
-        int perc95 = (int)Math.ceil(wgtAvgPercentile95);
-        int perc99 = (int)Math.ceil(wgtAvgPercentile99);
-
-        return new JankStat(pkg, totalStatsSince, totalTotalFrames,
-                totalJankyFrames, perc50, perc90, perc95, perc99,
-                totalNumMissedVsync, totalNumHighLatency, totalNumSlowUiThread, totalNumSlowBitmap,
-                totalNumSlowDraw, statHistory.size());
-    }
-
-    /**
-     * Returns a long String containing each JankStat object separated by a
-     * newline. Ideally, this would omit objects with zero rendered total
-     * frames, which is junk data.
-     */
-    public static String statsListToString (List<JankStat> statsList) {
-        StringBuilder result = new StringBuilder();
-        for (JankStat stats : statsList) {
-            result.append(stats.toString());
-            result.append("\n");
-        }
-
-        return result.toString();
-    }
-}
diff --git a/libraries/aupt-lib/src/android/support/test/aupt/LogGenerator.java b/libraries/aupt-lib/src/android/support/test/aupt/LogGenerator.java
new file mode 100644
index 0000000..7cd4fe0
--- /dev/null
+++ b/libraries/aupt-lib/src/android/support/test/aupt/LogGenerator.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.test.aupt;
+
+import android.app.Instrumentation;
+import android.util.Log;
+
+import java.io.File;
+import java.io.IOException;
+
+enum LogGenerator {
+    BUGREPORT(new BugreportGenerator()),
+    BUGREPORTZ(new BugreportzGenerator()),
+    GRAPHICS_STATS(new GraphicsGenerator()),
+    MEM_INFO(new CompactMemInfoGenerator()),
+    CPU_INFO(new CpuInfoGenerator()),
+    FRAGMENTATION(new FragmentationGenerator()),
+    ION_HEAP(new IonHeapGenerator()),
+    PAGETYPE_INFO(new PageTypeInfoGenerator()),
+    TRACE(new TraceGenerator());
+
+    private static final String TAG = "AuptDataCollector";
+
+    /** Save the output of a process to a log file with the given name template. */
+    private static void saveLog(
+            Instrumentation instr,
+            String command,
+            String template) throws IOException {
+        FilesystemUtil.saveProcessOutput(
+            instr,
+            command,
+            new File(FilesystemUtil.templateToFilename(template)));
+    }
+
+    /* Generator Types */
+
+    protected interface Generator {
+        void save(Instrumentation instr, String logDir)
+                throws IOException, InterruptedException;
+    }
+
+    private static class CompactMemInfoGenerator implements Generator {
+        @Override
+        public void save(Instrumentation instr, String logDir)
+                throws IOException, InterruptedException {
+            try {
+                saveLog(instr, "dumpsys meminfo -c -S", logDir + "/compact-meminfo-%s.txt");
+            } catch (IOException ioe) {
+                Log.w(TAG, "Error while saving dumpsys meminfo -c: " + ioe.getMessage());
+            }
+        }
+    }
+
+    private static class CpuInfoGenerator implements Generator {
+        @Override
+        public void save(Instrumentation instr, String logDir)
+                throws IOException, InterruptedException {
+            try {
+                saveLog(instr, "dumpsys cpuinfo", logDir + "/cpuinfo-%s.txt");
+            } catch (IOException ioe) {
+                Log.w(TAG, "Error while saving dumpsys cpuinfo : " + ioe.getMessage());
+            }
+        }
+    }
+
+    private static class BugreportGenerator implements Generator {
+        @Override
+        public void save(Instrumentation instr, String logDir)
+                throws IOException, InterruptedException {
+            try {
+                FilesystemUtil.saveBugreport(instr, logDir + "/bugreport-%s.txt");
+            } catch (IOException e) {
+                Log.w(TAG, String.format("Failed to take bugreport: %s", e.getMessage()));
+            }
+        }
+    }
+
+    private static class BugreportzGenerator implements Generator {
+        @Override
+        public void save(Instrumentation instr, String logDir)
+                throws IOException, InterruptedException {
+            try {
+                FilesystemUtil.saveBugreportz(instr);
+            } catch (IOException e) {
+                Log.w(TAG, String.format("Failed to take bugreport: %s", e.getMessage()));
+            }
+        }
+    }
+
+    private static class FragmentationGenerator implements Generator {
+        @Override
+        public void save(Instrumentation instr, String logDir)
+                throws IOException, InterruptedException {
+            try {
+                saveLog(instr, "cat /d/extfrag/unusable_index", logDir + "/unusable-index-%s.txt");
+            } catch (IOException e) {
+                Log.w(TAG, String.format("Failed to save frangmentation: %s", e.getMessage()));
+            }
+        }
+    }
+
+    private static class GraphicsGenerator implements Generator {
+        @Override
+        public void save(Instrumentation instr, String logDir)
+                throws IOException, InterruptedException {
+            try {
+                saveLog(instr, "dumpsys graphicsstats", logDir + "/graphics-%s.txt");
+            } catch (IOException e) {
+                Log.w(TAG, String.format("Failed to save graphicsstats: %s", e.getMessage()));
+            }
+        }
+    }
+
+    private static class IonHeapGenerator implements Generator {
+        @Override
+        public void save(Instrumentation instr, String logDir)
+                throws IOException, InterruptedException {
+            try {
+                saveLog(instr, "cat /d/ion/heaps/audio", logDir + "/ion-audio-%s.txt");
+                saveLog(instr, "cat /d/ion/heaps/system", logDir + "/ion-system-%s.txt");
+            } catch (IOException e) {
+                Log.w(TAG, String.format("Failed to save ION heap: %s", e.getMessage()));
+            }
+        }
+    }
+
+    private static class PageTypeInfoGenerator implements Generator {
+        @Override
+        public void save(Instrumentation instr, String logDir)
+                throws IOException, InterruptedException {
+            try {
+                saveLog(instr, "cat /proc/pagetypeinfo", logDir + "/pagetypeinfo-%s.txt");
+            } catch (IOException e) {
+                Log.w(TAG, String.format("Failed to save pagetypeinfo: %s", e.getMessage()));
+            }
+        }
+    }
+
+    private static class TraceGenerator implements Generator {
+        @Override
+        public void save(Instrumentation instr, String logDir)
+                throws IOException, InterruptedException {
+            try {
+                saveLog(instr, "cat /sys/kernel/debug/tracing/trace", logDir + "/trace-%s.txt");
+            } catch (IOException e) {
+                Log.w(TAG, String.format("Failed to save trace: %s", e.getMessage()));
+            }
+        }
+    }
+
+    // Individual LogGenerator instance methods
+    private final Generator mGenerator;
+
+    LogGenerator (Generator generator) {
+        mGenerator = generator;
+    }
+
+    public void save(Instrumentation instr, String logDir)
+            throws IOException, InterruptedException {
+        mGenerator.save(instr, logDir);
+    }
+}
diff --git a/libraries/aupt-lib/src/android/support/test/aupt/MemHealthRecord.java b/libraries/aupt-lib/src/android/support/test/aupt/MemHealthRecord.java
new file mode 100644
index 0000000..ef2fb563
--- /dev/null
+++ b/libraries/aupt-lib/src/android/support/test/aupt/MemHealthRecord.java
@@ -0,0 +1,521 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.test.aupt;
+
+import android.app.Instrumentation;
+import java.io.BufferedWriter;
+import java.io.ByteArrayOutputStream;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+class MemHealthRecord {
+    // Process State
+    private final String mProcName;
+    private final boolean mInForeground;
+
+    // Memory health state
+    private final long mTimeMs;
+    private final long mDalvikHeap;
+    private final long mNativeHeap;
+    private final long mPss;
+
+    // App summary metrics
+    private final long mAsJavaHeap;
+    private final long mAsNativeHeap;
+    private final long mAsCode;
+    private final long mAsStack;
+    private final long mAsGraphics;
+    private final long mAsOther;
+    private final long mAsSystem;
+    private final long mAsOverallPss;
+
+    public MemHealthRecord(String procName, long timeMs, long dalvikHeap, long nativeHeap, long pss,
+            long asJavaHeap, long asNativeHeap, long asCode, long asStack,
+            long asGraphics, long asOther, long asSystem, long asOverallPss,
+            boolean inForeground) {
+        mProcName = procName;
+        mTimeMs = timeMs;
+        mDalvikHeap = dalvikHeap;
+        mNativeHeap = nativeHeap;
+        mPss = pss;
+        mAsJavaHeap = asJavaHeap;
+        mAsNativeHeap = asNativeHeap;
+        mAsCode = asCode;
+        mAsStack = asStack;
+        mAsGraphics = asGraphics;
+        mAsOther = asOther;
+        mAsSystem = asSystem;
+        mAsOverallPss = asOverallPss;
+        mInForeground = inForeground;
+    }
+
+    public MemHealthRecord(
+            String procName, long timeMs, long dalvikHeap,
+            long nativeHeap, long pss, boolean inForeground) {
+        this(procName, timeMs, dalvikHeap, nativeHeap, pss, 0, 0, 0, 0, 0, 0, 0, 0, inForeground);
+    }
+
+    /* Static methods */
+
+    static List<MemHealthRecord> get(
+            Instrumentation instr,
+            List<String> procNames,
+            long timeMs,
+            List<String> foregroundProcs) throws IOException {
+
+        List<MemHealthRecord> records = new ArrayList<>();
+
+        for (String procName : procNames) {
+            String meminfo = getMeminfoOutput(instr, procName);
+            int nativeHeap = parseMeminfoLine(meminfo, "Native Heap\\s+\\d+\\s+(\\d+)");
+            int dalvikHeap = parseMeminfoLine(meminfo, "Dalvik Heap\\s+\\d+\\s+(\\d+)");
+            int pss = parseMeminfoLine(meminfo, "TOTAL\\s+(\\d+)");
+
+            int asJavaHeap = parseMeminfoLine(meminfo, "Java Heap:\\s+(\\d+)");
+            int asNativeHeap = parseMeminfoLine(meminfo, "Native Heap:\\s+(\\d+)");
+            int asCode = parseMeminfoLine(meminfo, "Code:\\s+(\\d+)");
+            int asStack = parseMeminfoLine(meminfo, "Stack:\\s+(\\d+)");
+            int asGraphics = parseMeminfoLine(meminfo, "Graphics:\\s+(\\d+)");
+            int asOther = parseMeminfoLine(meminfo, "Private Other:\\s+(\\d+)");
+            int asSystem = parseMeminfoLine(meminfo, "System:\\s+(\\d+)");
+            int asOverallPss = parseMeminfoLine(meminfo, "TOTAL:\\s+(\\d+)");
+
+            if (nativeHeap < 0 || dalvikHeap < 0 || pss < 0) {
+                continue;
+            }
+
+            records.add(new MemHealthRecord(
+                    procName, timeMs, dalvikHeap, nativeHeap, pss, asJavaHeap,
+                    asNativeHeap, asCode, asStack, asGraphics, asOther, asSystem,
+                    asOverallPss, foregroundProcs.contains(procName)));
+        }
+
+        return records;
+    }
+
+    static void saveVerbose(Collection<MemHealthRecord> allRecords, String fileName)
+            throws IOException {
+        PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(fileName, true)));
+
+        Map<String, List<MemHealthRecord>> fgRecords = getRecordMap(allRecords, true);
+        Map<String, List<MemHealthRecord>> bgRecords = getRecordMap(allRecords, false);
+
+        out.println("Foreground");
+
+        for (Map.Entry<String, List<MemHealthRecord>> entry : fgRecords.entrySet()) {
+            String procName = entry.getKey();
+            List<MemHealthRecord> records = entry.getValue();
+
+            List<Long> nativeHeap = getForegroundNativeHeap(records);
+            List<Long> dalvikHeap = getForegroundDalvikHeap(records);
+            List<Long> pss = getForegroundPss(records);
+            List<Long> asJavaHeap = getForegroundSummaryJavaHeap(records);
+            List<Long> asNativeHeap = getForegroundSummaryNativeHeap(records);
+            List<Long> asCode = getForegroundSummaryCode(records);
+            List<Long> asStack = getForegroundSummaryStack(records);
+            List<Long> asGraphics = getForegroundSummaryGraphics(records);
+            List<Long> asOther = getForegroundSummaryOther(records);
+            List<Long> asSystem = getForegroundSummarySystem(records);
+            List<Long> asOverallPss = getForegroundSummaryOverallPss(records);
+
+            // nativeHeap, dalvikHeap, and pss all have the same size, just use one
+            if (nativeHeap.isEmpty()) {
+                continue;
+            }
+
+            out.println(procName);
+            out.printf("Average Native Heap: %d\n", getAverage(nativeHeap));
+            out.printf("Average Dalvik Heap: %d\n", getAverage(dalvikHeap));
+            out.printf("Average PSS: %d\n", getAverage(pss));
+            out.printf("Peak Native Heap: %d\n", getMax(nativeHeap));
+            out.printf("Peak Dalvik Heap: %d\n", getMax(dalvikHeap));
+            out.printf("Peak PSS: %d\n", getMax(pss));
+            out.printf("Count %d\n", nativeHeap.size());
+
+            out.printf("Average Summary Java Heap: %d\n", getAverage(asJavaHeap));
+            out.printf("Average Summary Native Heap: %d\n", getAverage(asNativeHeap));
+            out.printf("Average Summary Code: %d\n", getAverage(asCode));
+            out.printf("Average Summary Stack: %d\n", getAverage(asStack));
+            out.printf("Average Summary Graphics: %d\n", getAverage(asGraphics));
+            out.printf("Average Summary Other: %d\n", getAverage(asOther));
+            out.printf("Average Summary System: %d\n", getAverage(asSystem));
+            out.printf("Average Summary Overall Pss: %d\n", getAverage(asOverallPss));
+        }
+
+        out.println("Background");
+        for (Map.Entry<String, List<MemHealthRecord>> entry : fgRecords.entrySet()) {
+            String procName = entry.getKey();
+            List<MemHealthRecord> records = entry.getValue();
+
+            List<Long> nativeHeap = getBackgroundNativeHeap(records);
+            List<Long> dalvikHeap = getBackgroundDalvikHeap(records);
+            List<Long> pss = getBackgroundPss(records);
+            List<Long> asJavaHeap = getBackgroundSummaryJavaHeap(records);
+            List<Long> asNativeHeap = getBackgroundSummaryNativeHeap(records);
+            List<Long> asCode = getBackgroundSummaryCode(records);
+            List<Long> asStack = getBackgroundSummaryStack(records);
+            List<Long> asGraphics = getBackgroundSummaryGraphics(records);
+            List<Long> asOther = getBackgroundSummaryOther(records);
+            List<Long> asSystem = getBackgroundSummarySystem(records);
+            List<Long> asOverallPss = getBackgroundSummaryOverallPss(records);
+
+            // nativeHeap, dalvikHeap, and pss all have the same size, just use one
+            if (nativeHeap.isEmpty()) {
+                continue;
+            }
+
+            out.println(procName);
+            out.printf("Average Native Heap: %d\n", getAverage(nativeHeap));
+            out.printf("Average Dalvik Heap: %d\n", getAverage(dalvikHeap));
+            out.printf("Average PSS: %d\n", getAverage(pss));
+            out.printf("Peak Native Heap: %d\n", getMax(nativeHeap));
+            out.printf("Peak Dalvik Heap: %d\n", getMax(dalvikHeap));
+            out.printf("Peak PSS: %d\n", getMax(pss));
+            out.printf("Count %d\n", nativeHeap.size());
+
+            out.printf("Average Summary Java Heap: %d\n", getAverage(asJavaHeap));
+            out.printf("Average Summary Native Heap: %d\n", getAverage(asNativeHeap));
+            out.printf("Average Summary Code: %d\n", getAverage(asCode));
+            out.printf("Average Summary Stack: %d\n", getAverage(asStack));
+            out.printf("Average Summary Graphics: %d\n", getAverage(asGraphics));
+            out.printf("Average Summary Other: %d\n", getAverage(asOther));
+            out.printf("Average Summary System: %d\n", getAverage(asSystem));
+            out.printf("Average Summary Overall Pss: %d\n", getAverage(asOverallPss));
+        }
+
+        out.close();
+    }
+
+    /**
+     * NOTE (rsloan): I've meaningfully changed this format because the previous iteration was a
+     *                horrific mix of CSV and not-CSV
+     */
+    static void saveCsv(Collection<MemHealthRecord> allRecords, String fileName) throws IOException{
+        PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(fileName, true)));
+
+        out.printf("name,time,native_heap,dalvik_heap,pss,context\n");
+        for (MemHealthRecord record : allRecords) {
+            out.printf("%s,%d,%d,%d,%s\n",
+                    record.mProcName, record.mTimeMs, record.mNativeHeap,
+                    record.mDalvikHeap, record.mInForeground ? "foreground" : "background");
+        }
+
+        out.close();
+    }
+
+    /* Getters defined on a Collection<MemHealthRecord> */
+
+    public static List<Long> getForegroundDalvikHeap(Collection<MemHealthRecord> samples) {
+        List<Long> ret = new ArrayList<>(samples.size());
+        for (MemHealthRecord sample : samples) {
+            if (sample.mInForeground) {
+                ret.add(sample.mDalvikHeap);
+            }
+        }
+        return ret;
+    }
+
+    public static List<Long> getBackgroundDalvikHeap(Collection<MemHealthRecord> samples) {
+        List<Long> ret = new ArrayList<>(samples.size());
+        for (MemHealthRecord sample : samples) {
+            if (!sample.mInForeground) {
+                ret.add(sample.mDalvikHeap);
+            }
+        }
+        return ret;
+    }
+
+    public static List<Long> getForegroundNativeHeap(Collection<MemHealthRecord> samples) {
+        List<Long> ret = new ArrayList<>(samples.size());
+        for (MemHealthRecord sample : samples) {
+            if (sample.mInForeground) {
+                ret.add(sample.mNativeHeap);
+            }
+        }
+        return ret;
+    }
+
+    public static List<Long> getBackgroundNativeHeap(Collection<MemHealthRecord> samples) {
+        List<Long> ret = new ArrayList<>(samples.size());
+        for (MemHealthRecord sample : samples) {
+            if (!sample.mInForeground) {
+                ret.add(sample.mNativeHeap);
+            }
+        }
+        return ret;
+    }
+
+    public static List<Long> getForegroundPss(Collection<MemHealthRecord> samples) {
+        List<Long> ret = new ArrayList<>(samples.size());
+        for (MemHealthRecord sample : samples) {
+            if (sample.mInForeground) {
+                ret.add(sample.mPss);
+            }
+        }
+        return ret;
+    }
+
+    public static List<Long> getBackgroundPss(Collection<MemHealthRecord> samples) {
+        List<Long> ret = new ArrayList<>(samples.size());
+        for (MemHealthRecord sample : samples) {
+            if (!sample.mInForeground) {
+                ret.add(sample.mPss);
+            }
+        }
+        return ret;
+    }
+
+    public static List<Long> getForegroundSummaryJavaHeap(Collection<MemHealthRecord> samples) {
+        List<Long> ret = new ArrayList<>(samples.size());
+        for (MemHealthRecord sample : samples) {
+            if (sample.mInForeground) {
+                ret.add(sample.mAsJavaHeap);
+            }
+        }
+        return ret;
+    }
+
+    public static List<Long> getBackgroundSummaryJavaHeap(Collection<MemHealthRecord> samples) {
+        List<Long> ret = new ArrayList<>(samples.size());
+        for (MemHealthRecord sample : samples) {
+            if (!sample.mInForeground) {
+                ret.add(sample.mAsJavaHeap);
+            }
+        }
+        return ret;
+    }
+
+    public static List<Long> getForegroundSummaryNativeHeap(
+            Collection<MemHealthRecord> samples) {
+        List<Long> ret = new ArrayList<>(samples.size());
+        for (MemHealthRecord sample : samples) {
+            if (sample.mInForeground) {
+                ret.add(sample.mAsNativeHeap);
+            }
+        }
+        return ret;
+    }
+
+    public static List<Long> getBackgroundSummaryNativeHeap(
+            Collection<MemHealthRecord> samples) {
+        List<Long> ret = new ArrayList<>(samples.size());
+        for (MemHealthRecord sample : samples) {
+            if (!sample.mInForeground) {
+                ret.add(sample.mAsNativeHeap);
+            }
+        }
+        return ret;
+    }
+
+    public static List<Long> getForegroundSummaryCode(Collection<MemHealthRecord> samples) {
+        List<Long> ret = new ArrayList<>(samples.size());
+        for (MemHealthRecord sample : samples) {
+            if (sample.mInForeground) {
+                ret.add(sample.mAsCode);
+            }
+        }
+        return ret;
+    }
+
+    public static List<Long> getBackgroundSummaryCode(Collection<MemHealthRecord> samples) {
+        List<Long> ret = new ArrayList<>(samples.size());
+        for (MemHealthRecord sample : samples) {
+            if (!sample.mInForeground) {
+                ret.add(sample.mAsCode);
+            }
+        }
+        return ret;
+    }
+
+    public static List<Long> getForegroundSummaryStack(Collection<MemHealthRecord> samples) {
+        List<Long> ret = new ArrayList<>(samples.size());
+        for (MemHealthRecord sample : samples) {
+            if (sample.mInForeground) {
+                ret.add(sample.mAsStack);
+            }
+        }
+        return ret;
+    }
+
+    public static List<Long> getBackgroundSummaryStack(Collection<MemHealthRecord> samples) {
+        List<Long> ret = new ArrayList<>(samples.size());
+        for (MemHealthRecord sample : samples) {
+            if (!sample.mInForeground) {
+                ret.add(sample.mAsStack);
+            }
+        }
+        return ret;
+    }
+
+    public static List<Long> getForegroundSummaryGraphics(Collection<MemHealthRecord> samples) {
+        List<Long> ret = new ArrayList<>(samples.size());
+        for (MemHealthRecord sample : samples) {
+            if (sample.mInForeground) {
+                ret.add(sample.mAsGraphics);
+            }
+        }
+        return ret;
+    }
+
+    public static List<Long> getBackgroundSummaryGraphics(Collection<MemHealthRecord> samples) {
+        List<Long> ret = new ArrayList<>(samples.size());
+        for (MemHealthRecord sample : samples) {
+            if (!sample.mInForeground) {
+                ret.add(sample.mAsGraphics);
+            }
+        }
+        return ret;
+    }
+
+    public static List<Long> getForegroundSummaryOther(Collection<MemHealthRecord> samples) {
+        List<Long> ret = new ArrayList<>(samples.size());
+        for (MemHealthRecord sample : samples) {
+            if (sample.mInForeground) {
+                ret.add(sample.mAsOther);
+            }
+        }
+        return ret;
+    }
+
+    public static List<Long> getBackgroundSummaryOther(Collection<MemHealthRecord> samples) {
+        List<Long> ret = new ArrayList<>(samples.size());
+        for (MemHealthRecord sample : samples) {
+            if (!sample.mInForeground) {
+                ret.add(sample.mAsOther);
+            }
+        }
+        return ret;
+    }
+
+    public static List<Long> getForegroundSummarySystem(Collection<MemHealthRecord> samples) {
+        List<Long> ret = new ArrayList<>(samples.size());
+        for (MemHealthRecord sample : samples) {
+            if (sample.mInForeground) {
+                ret.add(sample.mAsSystem);
+            }
+        }
+        return ret;
+    }
+
+    public static List<Long> getBackgroundSummarySystem(Collection<MemHealthRecord> samples) {
+        List<Long> ret = new ArrayList<>(samples.size());
+        for (MemHealthRecord sample : samples) {
+            if (!sample.mInForeground) {
+                ret.add(sample.mAsSystem);
+            }
+        }
+        return ret;
+    }
+
+    public static List<Long> getForegroundSummaryOverallPss(
+            Collection<MemHealthRecord> samples) {
+        List<Long> ret = new ArrayList<>(samples.size());
+        for (MemHealthRecord sample : samples) {
+            if (sample.mInForeground) {
+                ret.add(sample.mAsOverallPss);
+            }
+        }
+        return ret;
+    }
+
+    public static List<Long> getBackgroundSummaryOverallPss(
+            Collection<MemHealthRecord> samples) {
+        List<Long> ret = new ArrayList<>(samples.size());
+        for (MemHealthRecord sample : samples) {
+            if (!sample.mInForeground) {
+                ret.add(sample.mAsOverallPss);
+            }
+        }
+        return ret;
+    }
+
+    /* Utility Methods */
+
+    private static Long getMax(Collection<Long> samples) {
+        Long max = null;
+        for (Long sample : samples) {
+            if (max == null || sample > max) {
+                max = sample;
+            }
+        }
+        return max;
+    }
+
+    private static Long getAverage(Collection<Long> samples) {
+        if (samples.size() == 0) {
+            return null;
+        }
+
+        double sum = 0;
+        for (Long sample : samples) {
+            sum += sample;
+        }
+        return (long) (sum / samples.size());
+    }
+
+    private static int parseMeminfoLine(String meminfo, String pattern)
+    {
+        Pattern p = Pattern.compile(pattern);
+        Matcher m = p.matcher(meminfo);
+        if (m.find()) {
+            return Integer.parseInt(m.group(1));
+        } else {
+            return -1;
+        }
+    }
+
+    public static String getMeminfoOutput(Instrumentation instr, String processName)
+            throws IOException {
+        return getProcessOutput(instr, "dumpsys meminfo " + processName);
+    }
+
+    public static String getProcessOutput(Instrumentation instr, String command)
+            throws IOException {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        FilesystemUtil.saveProcessOutput(instr, command, baos);
+        baos.close();
+        return baos.toString();
+    }
+
+    private static Map<String, List<MemHealthRecord>> getRecordMap(
+            Collection<MemHealthRecord> allRecords,
+            boolean inForeground) {
+
+        Map<String, List<MemHealthRecord>> records = new HashMap<>();
+
+        for (MemHealthRecord record : allRecords) {
+            if (record.mInForeground == inForeground) {
+                if (!records.containsKey(record.mProcName)) {
+                    records.put(record.mProcName, new ArrayList<MemHealthRecord>());
+                }
+
+                records.get(record.mProcName).add(record);
+            }
+        }
+
+        return records;
+    }
+
+}
diff --git a/libraries/aupt-lib/src/android/support/test/aupt/Scheduler.java b/libraries/aupt-lib/src/android/support/test/aupt/Scheduler.java
new file mode 100644
index 0000000..02c2d8e
--- /dev/null
+++ b/libraries/aupt-lib/src/android/support/test/aupt/Scheduler.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.test.aupt;
+
+import junit.framework.TestCase;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.Random;
+
+/**
+ * A Scheduler produces an execution ordering for our test cases.
+ *
+ * For example, the Sequential scheduler will just repeat our test cases a fixed number of times.
+ */
+public abstract class Scheduler {
+
+    /** Shuffle and/or iterate through a list of test cases. */
+    public final Iterable<TestCase> apply(final List<TestCase> cases) {
+        return new Iterable<TestCase>() {
+            public Iterator<TestCase> iterator() {
+                return applyInternal(cases);
+            }
+        };
+    }
+
+    /**
+     * A Scheduler that just loops through test cases a fixed number of times.
+     *
+     * @param iterations the number of times to loop through every test case.
+     */
+    public static Scheduler sequential(Long iterations) {
+        return new Sequential(iterations);
+    }
+
+    /**
+     * A Scheduler that permutes the test case list, then just iterates.
+     *
+     * @param iterations the number of times to loop through every test case.
+     * @param random the Random to use: with the same random, we'll return the same ordering.
+     */
+    public static Scheduler shuffled(Random random, Long iterations) {
+        return new Shuffled(random, iterations);
+    }
+
+    /** Private interface to Scheduler::apply */
+    protected abstract Iterator<TestCase> applyInternal(List<TestCase> cases);
+
+    private static class Sequential extends Scheduler {
+        private final Long mIterations;
+
+        Sequential(Long iterations) {
+            mIterations = iterations;
+        }
+
+        protected Iterator<TestCase> applyInternal (final List<TestCase> cases) {
+            return new Iterator<TestCase>() {
+                private int count = 0;
+
+                public boolean hasNext() {
+                    return count < (cases.size() * mIterations);
+                }
+
+                public TestCase next() {
+                    return cases.get(count++ % cases.size());
+                }
+
+                public void remove() { }
+            };
+        }
+    }
+
+    private static class Shuffled extends Scheduler {
+        private final Random mRandom;
+        private final Long mIterations;
+
+        Shuffled(Random random, Long iterations) {
+            mRandom = random;
+            mIterations = iterations;
+        }
+
+        /**
+         * Find a GCD by the nieve Euclidean Algorithm
+         * TODO: get this from guava or some other library
+         */
+        private int gcd(final int _a, final int _b) {
+            int a = _a;
+            int b = _b;
+
+            while (b > 0) {
+                int tmp = b;
+                b = a % b;
+                a = tmp;
+            }
+
+            return a;
+        }
+
+        /**
+         * Find a random number relatively prime to our modulus
+         * TODO: get this from guava or some other library
+         */
+        private int randomRelPrime(Integer modulus) {
+            if (modulus <= 1) {
+                return 1;
+            } else {
+                int x = 0;
+
+                // Sample random numbers until we get something coprime to our modulus
+                while (gcd(x, modulus) != 1) {
+                    x = mRandom.nextInt() % modulus;
+                }
+
+                return x % modulus;
+            }
+        }
+
+        /**
+         * Return the tests in a shuffled order using a simple linear congruential generator: i.e.
+         * the elements are permuted by (a x + b % n), will produce a permutation of the elements of n
+         * iff a is coprime to n.
+         * <p>
+         * The reason to do this is that it also produces a permutation each cases.size() rounds, which
+         * is *not* the case for Collections.shuffle(); and implementing a comparable iterator with
+         * those primitives is somewhat less elegant.
+         */
+        protected Iterator<TestCase> applyInternal(final List<TestCase> cases) {
+            final int a = randomRelPrime(cases.size());
+            final int b = Math.abs(mRandom.nextInt());
+
+            return new Iterator<TestCase>() {
+                private int count = 0;
+
+                public boolean hasNext() {
+                    return count < (cases.size() * mIterations);
+                }
+
+                public TestCase next() {
+                    return cases.get((a * (count++) + b) % cases.size());
+                }
+
+                public void remove() {
+                }
+            };
+        }
+    }
+}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractAngryBirdsHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractAngryBirdsHelper.java
deleted file mode 100644
index dc1f037..0000000
--- a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractAngryBirdsHelper.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.platform.test.helpers;
-
-import android.app.Instrumentation;
-
-public abstract class AbstractAngryBirdsHelper extends AbstractStandardAppHelper {
-
-    public AbstractAngryBirdsHelper(Instrumentation instr) {
-        super(instr);
-    }
-
-    /**
-     * Setup expectation: Angry Birds open and on main menu
-     *
-     * This method will set up game demo by going to level selection menu
-     */
-    public abstract void setUpDemo();
-
-    /**
-     * Setup expectation: Angry Birds open and on level selection menu
-     *
-     * This method plays a game demo for demoDurationInMinutes minutes
-     * @param demoDurationInMinutes: game demo duration in minutes
-     */
-    public abstract void playDemo(int demoDurationInMinutes);
-
-    /**
-     * Setup expectation: Angry Birds open and on level selection menu
-     *
-     * This method goes back to the main menu
-     */
-    public abstract void tearDownDemo();
-}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractFacebookHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractFacebookHelper.java
deleted file mode 100644
index a532263..0000000
--- a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractFacebookHelper.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.platform.test.helpers;
-
-import android.app.Instrumentation;
-import android.support.test.uiautomator.Direction;
-
-public abstract class AbstractFacebookHelper extends AbstractStandardAppHelper {
-
-    public AbstractFacebookHelper(Instrumentation instr) {
-        super(instr);
-    }
-
-    /**
-     * Setup expectations: Facebook is on the home page.
-     *
-     * This method scrolls the home page.
-     *
-     * @param dir the direction to scroll
-     */
-    public abstract void scrollHomePage(Direction dir);
-
-    /**
-     * Setup expectations: Facebook app is open.
-     *
-     * This method keeps pressing the back button until Facebook is on the homepage.
-     */
-    public abstract void goToHomePage();
-
-    /**
-     * Setup expectations: Facebook app is on the home page.
-     *
-     * This method moves the Facebook app to the News Feed tab of the home page.
-     */
-    public abstract void goToNewsFeed();
-
-    /**
-     * Setup expectations: Facebook is on the News Feed tab of the home page.
-     *
-     * This method moves the Facebook app to the status update page.
-     */
-    public abstract void goToStatusUpdate();
-
-    /**
-     * Setup expectations: Facebook is on the status update page.
-     *
-     * This method clicks on the status update text field to move the keyboard cursor there
-     */
-    public abstract void clickStatusUpdateTextField();
-
-    /**
-     * Setup expections: Facebook is on the status update page.
-     *
-     * This method sets the status update text.
-     *
-     * @param statusText text for status update
-     */
-    public abstract void setStatusText(String statusText);
-
-    /**
-     * Setup expectations: Facebook is on the status update page.
-     *
-     * This method posts the status update.
-     */
-    public abstract void postStatusUpdate();
-
-    /**
-     * Setup expectations: Facebook app is on the login page.
-     *
-     * This method attempts to log in using the specified username and password.
-     *
-     * @param username username of Facebook account
-     * @param password password of Facebook account
-     */
-    public abstract void login(String username, String password);
-}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractFlightDemoHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractFlightDemoHelper.java
deleted file mode 100644
index 84c31d9..0000000
--- a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractFlightDemoHelper.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.platform.test.helpers;
-
-import android.app.Instrumentation;
-
-public abstract class AbstractFlightDemoHelper extends AbstractStandardAppHelper {
-
-    public AbstractFlightDemoHelper(Instrumentation instr) {
-        super(instr);
-    }
-
-    /**
-     * Setup expectation: On the opening screen.
-     *
-     * Best effort attempt to start the flight simulator demo
-     */
-    public abstract void startDemo();
-
-    /**
-     * Setup expectation: Currently running the flight simulator demo
-     *
-     * Best effort attempt to stop the flight simulator demo
-     */
-    public abstract void stopDemo();
-}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractRecentsHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractRecentsHelper.java
deleted file mode 100644
index 7a1a85b..0000000
--- a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractRecentsHelper.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.platform.test.helpers;
-
-import android.app.Instrumentation;
-import android.support.test.uiautomator.Direction;
-
-public abstract class AbstractRecentsHelper extends AbstractStandardAppHelper {
-
-    public AbstractRecentsHelper(Instrumentation instr) {
-        super(instr);
-    }
-
-    /**
-     * Setup expectations: "Recents" is open.
-     *
-     * Flings the recent apps in the specified direction.
-     * @param dir the direction for the apps to move
-     */
-    public abstract void flingRecents(Direction dir);
-}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractRedditHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractRedditHelper.java
deleted file mode 100644
index 7cf83a0..0000000
--- a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractRedditHelper.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.platform.test.helpers;
-
-import android.app.Instrumentation;
-import android.support.test.uiautomator.Direction;
-
-public abstract class AbstractRedditHelper extends AbstractStandardAppHelper {
-
-    public AbstractRedditHelper(Instrumentation instr) {
-        super(instr);
-    }
-
-    /*
-     * Setup expectations: Reddit app is open.
-     *
-     * This method moves the Reddit app to the front page.
-     */
-    public abstract void goToFrontPage();
-
-    /*
-     * Setup expectations: Reddit app is on the front pages.
-     *
-     * This method moves the Reddit app to the first visible article's comment page.
-     */
-    public abstract void goToFirstArticleComments();
-
-    /*
-     * Setup expectations: Reddit app is on the front page.
-     *
-     * This method scrolls the front page.
-     *
-     * @param direction Direction in which to scroll, must be UP or DOWN
-     * @param percent   Percent of page to scroll
-     * @return boolean  Whether the page can still scroll in the given direction
-     */
-    public abstract boolean scrollFrontPage(Direction direction, float percent);
-
-    /*
-     * Setup expectations: Reddit app is on an article's comment page.
-     *
-     * This method scrolls the comment page.
-     *
-     * @param direction Direction in which to scroll, must be UP or DOWN
-     * @param percent   Percent of page to scroll
-     * @return boolean  Whether the page can still scroll in the given direction
-     */
-    public abstract boolean scrollCommentPage(Direction direction, float percent);
-}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractSlackerHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractSlackerHelper.java
deleted file mode 100644
index caceb0a..0000000
--- a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractSlackerHelper.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.platform.test.helpers;
-
-import android.app.Instrumentation;
-
-public abstract class AbstractSlackerHelper extends AbstractStandardAppHelper {
-
-    public AbstractSlackerHelper(Instrumentation instr) {
-        super(instr);
-    }
-
-    /**
-     * Setup expectation: Slacker is on page with playable radio channels
-     *
-     * This method starts playing one of the playable radio channels.
-     * If app is already on channel page, this function starts playing
-     * the radio on the current page. If it cannot start streaming a radio
-     * channel, it throws an exception.
-     */
-    public abstract void startAnyChannel();
-
-    /**
-     * Setup expectation: Slacker is on channel page
-     *
-     * This method pauses the audio streaming and returns to main page.
-     * If app is already on main page, this function does nothing.  If it
-     * cannot stop streamming channel, it throws an exception.
-     */
-    public abstract void stopChannel();
-}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractTuneInHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractTuneInHelper.java
deleted file mode 100644
index 1e5ec95..0000000
--- a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractTuneInHelper.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.platform.test.helpers;
-
-import android.app.Instrumentation;
-
-public abstract class AbstractTuneInHelper extends AbstractStandardAppHelper {
-
-    public AbstractTuneInHelper(Instrumentation instr) {
-        super(instr);
-    }
-
-    /**
-     * Setup expectation: TuneIn app is open, originally on Browse Page
-     *
-     * This method attempts a few times until go back to Browse Page
-     * and assert fails if it doesn't end up on Browse Page
-     */
-    public abstract void goToBrowsePage();
-
-    /**
-     * Setup expectation: TuneIn is on Browse page
-     *
-     * This method blocks until on local radio page
-     */
-    public abstract void goToLocalRadio();
-
-    /**
-     * Setup expectation: TuneIn is on Local Radio page
-     *
-     * This method selects the ith FM from the radio list
-     * and goes to radio profile page
-     * @param i ith FM
-     */
-    public abstract void selectFM(int i);
-
-    /**
-     * Setup expectation: TuneIn is on radio profile page
-     *
-     * This method starts playing the radio channel
-     */
-    public abstract void startChannel();
-
-    /**
-     * Setup expectation: TuneIn is on channel page
-     *
-     * This method stops the channel and stays on the page
-     */
-    public abstract void stopChannel();
-}
diff --git a/libraries/launcher-helper/Android.mk b/libraries/launcher-helper/Android.mk
index bd9d62f..9c45503 100644
--- a/libraries/launcher-helper/Android.mk
+++ b/libraries/launcher-helper/Android.mk
@@ -18,7 +18,9 @@
 include $(CLEAR_VARS)
 
 LOCAL_MODULE := launcher-helper-lib
-LOCAL_JAVA_LIBRARIES := ub-uiautomator
+LOCAL_JAVA_LIBRARIES := ub-uiautomator \
+    android-support-test \
+    activity-helper
 LOCAL_STATIC_JAVA_LIBRARIES := dpad-util
 LOCAL_SDK_VERSION := 21
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
diff --git a/libraries/launcher-helper/src/android/support/test/launcherhelper/AllAppsScreenHelper.java b/libraries/launcher-helper/src/android/support/test/launcherhelper/AllAppsScreenHelper.java
new file mode 100644
index 0000000..56ec6df
--- /dev/null
+++ b/libraries/launcher-helper/src/android/support/test/launcherhelper/AllAppsScreenHelper.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.test.launcherhelper;
+
+import android.app.Instrumentation;
+import android.app.UiAutomation;
+import android.content.pm.PackageManager;
+import android.content.Context;
+import android.os.RemoteException;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.launcherhelper.ILauncherStrategy;
+import android.support.test.launcherhelper.LauncherStrategyFactory;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.UiObjectNotFoundException;
+import android.support.test.uiautomator.Until;
+import android.system.helpers.ActivityHelper;
+import android.test.InstrumentationTestCase;
+
+import java.io.File;
+import java.io.IOException;
+
+import org.junit.Assert;
+
+public class AllAppsScreenHelper {
+
+    private static final int TIMEOUT = 3000;
+    private static final int LONG_TIMEOUT = 10000;
+    private UiDevice mDevice;
+    private Instrumentation mInstrumentation;
+    private ILauncherStrategy mLauncherStrategy = LauncherStrategyFactory
+            .getInstance(mDevice).getLauncherStrategy();
+    private String allApps = "apps_view";
+    private String appsListView = "apps_list_view";
+    private String searchBox = "search_box_input";
+    private ActivityHelper mActivityHelper;
+
+    public AllAppsScreenHelper() {
+        mInstrumentation = InstrumentationRegistry.getInstrumentation();
+        mActivityHelper = ActivityHelper.getInstance();
+        mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+    }
+
+    public String getLauncherPackage() {
+        return mDevice.getLauncherPackageName();
+    }
+
+    public void launchAllAppsScreen() {
+        mDevice.pressHome();
+        mDevice.findObject(mLauncherStrategy.getAllAppsButtonSelector()).click();
+        mDevice.wait(Until.hasObject(By.res(getLauncherPackage(), allApps)), TIMEOUT);
+    }
+
+    public void searchAllAppsScreen(String searchString,
+        String[] appNamesExpected) throws Exception {
+        launchAllAppsScreen();
+        UiObject2 searchBoxObject = mDevice.wait(Until.findObject
+                (By.res(getLauncherPackage(), searchBox)), TIMEOUT);
+        searchBoxObject.setText(searchString);
+        for (String appName : appNamesExpected) {
+            Assert.assertNotNull("The following app couldn't be found in the search results: "
+                    + appName, mDevice.wait(Until.findObject
+                    (By.text(appName)), TIMEOUT));
+        }
+    }
+}
diff --git a/libraries/launcher-helper/src/android/support/test/launcherhelper/BaseLauncher3Strategy.java b/libraries/launcher-helper/src/android/support/test/launcherhelper/BaseLauncher3Strategy.java
index 8bac06e..492519f 100644
--- a/libraries/launcher-helper/src/android/support/test/launcherhelper/BaseLauncher3Strategy.java
+++ b/libraries/launcher-helper/src/android/support/test/launcherhelper/BaseLauncher3Strategy.java
@@ -88,7 +88,7 @@
             // taps on the "apps" button at the bottom of the screen
             UiObject2 allAppsButton =
                     mDevice.wait(Until.findObject(getAllAppsButtonSelector()), 2000);
-            Assert.assertNotNull("openAllApps: did not find open all apps button.");
+            Assert.assertNotNull("openAllApps: did not find open all apps button.", allAppsButton);
             allAppsButton.click();
             // wait until hotseat disappears, so that we know that we are no longer on home screen
             mDevice.wait(Until.gone(getHotSeatSelector()), 2000);
diff --git a/libraries/launcher-helper/src/android/support/test/launcherhelper/ILeanbackLauncherStrategy.java b/libraries/launcher-helper/src/android/support/test/launcherhelper/ILeanbackLauncherStrategy.java
index 54fccde..28b915b 100644
--- a/libraries/launcher-helper/src/android/support/test/launcherhelper/ILeanbackLauncherStrategy.java
+++ b/libraries/launcher-helper/src/android/support/test/launcherhelper/ILeanbackLauncherStrategy.java
@@ -16,12 +16,13 @@
 
 package android.support.test.launcherhelper;
 
+import android.app.Instrumentation;
 import android.support.test.uiautomator.BySelector;
 import android.support.test.uiautomator.UiDevice;
 import android.support.test.uiautomator.UiObject2;
 
 /**
- * Defines the common use cases a leanback launcher UI automation helper should fulfill.
+ * Defines the common use cases that any types of TV launcher UI automation helper should fulfill.
  * <p>Class will be instantiated by {@link LauncherStrategyFactory} based on current launcher
  * package, and a {@link UiDevice} instance will be provided via {@link #setUiDevice(UiDevice)}
  * method.
@@ -29,7 +30,12 @@
 public interface ILeanbackLauncherStrategy extends ILauncherStrategy {
 
     /**
-     * Searches for a given query on leanback launcher
+     * Sets an instance of instrumentation
+     */
+    public void setInstrumentation(Instrumentation instrumentation);
+
+    /**
+     * Searches for a given query on TV launcher
      */
     public void search(String query);
 
@@ -76,7 +82,8 @@
     public BySelector getNowPlayingCardSelector();
 
     /**
-     * Returns a {@link UiObject2} describing the focused search row
+     * Returns a {@link UiObject2} describing the focused search row,
+     * or the top row on new TV Launcher
      * @return
      */
     public UiObject2 selectSearchRow();
diff --git a/libraries/launcher-helper/src/android/support/test/launcherhelper/Launcher3Strategy.java b/libraries/launcher-helper/src/android/support/test/launcherhelper/Launcher3Strategy.java
index 6d7fbfc..f6af664 100644
--- a/libraries/launcher-helper/src/android/support/test/launcherhelper/Launcher3Strategy.java
+++ b/libraries/launcher-helper/src/android/support/test/launcherhelper/Launcher3Strategy.java
@@ -50,6 +50,6 @@
      */
     @Override
     public BySelector getAllAppsButtonSelector() {
-        return By.desc("Apps");
+        return By.res(getSupportedLauncherPackage(), "all_apps_handle");
     }
 }
diff --git a/libraries/launcher-helper/src/android/support/test/launcherhelper/LauncherStrategyFactory.java b/libraries/launcher-helper/src/android/support/test/launcherhelper/LauncherStrategyFactory.java
index cab96cd..c6a52c0 100644
--- a/libraries/launcher-helper/src/android/support/test/launcherhelper/LauncherStrategyFactory.java
+++ b/libraries/launcher-helper/src/android/support/test/launcherhelper/LauncherStrategyFactory.java
@@ -46,6 +46,7 @@
         registerLauncherStrategy(PixelCLauncherStrategy.class);
         registerLauncherStrategy(LeanbackLauncherStrategy.class);
         registerLauncherStrategy(WearLauncherStrategy.class);
+        registerLauncherStrategy(TvLauncherStrategy.class);
     }
 
     /**
@@ -86,16 +87,22 @@
      * {@link ILauncherStrategy} maybe registered via
      * {@link LauncherStrategyFactory#registerLauncherStrategy(Class)} by identifying the
      * launcher package name supported
+     * @throw RuntimeException if no valid launcher strategy is found
      * @return
      */
     public ILauncherStrategy getLauncherStrategy() {
         String launcherPkg = mUiDevice.getLauncherPackageName();
-        return mInstanceMap.get(launcherPkg);
+        if (mInstanceMap.containsKey(launcherPkg)) {
+            return mInstanceMap.get(launcherPkg);
+        } else {
+            throw new RuntimeException(String.format(
+                    "Could not find a launcher strategy for package, %s", launcherPkg));
+        }
     }
 
     /**
-     * Retrieves a {@link ILeanbackLauncherStrategy} that supports the current default leanback
-     * launcher
+     * Retrieves a {@link ILeanbackLauncherStrategy} that supports the current default launcher
+     * for TV. Either Leanback Launcher or new TV Launcher
      * @return
      */
     public ILeanbackLauncherStrategy getLeanbackLauncherStrategy() {
@@ -103,6 +110,6 @@
         if (launcherStrategy instanceof ILeanbackLauncherStrategy) {
             return (ILeanbackLauncherStrategy)launcherStrategy;
         }
-        throw new RuntimeException("This LauncherStrategy is not for leanback launcher.");
+        throw new RuntimeException("This LauncherStrategy is suitable for TV.");
     }
 }
diff --git a/libraries/launcher-helper/src/android/support/test/launcherhelper/LeanbackLauncherStrategy.java b/libraries/launcher-helper/src/android/support/test/launcherhelper/LeanbackLauncherStrategy.java
index cac78a1..2ecc0c0 100644
--- a/libraries/launcher-helper/src/android/support/test/launcherhelper/LeanbackLauncherStrategy.java
+++ b/libraries/launcher-helper/src/android/support/test/launcherhelper/LeanbackLauncherStrategy.java
@@ -16,6 +16,9 @@
 
 package android.support.test.launcherhelper;
 
+import android.app.Instrumentation;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
 import android.graphics.Point;
 import android.os.RemoteException;
 import android.os.SystemClock;
@@ -40,10 +43,11 @@
     private static final int MAX_SCROLL_ATTEMPTS = 20;
     private static final int APP_LAUNCH_TIMEOUT = 10000;
     private static final int SHORT_WAIT_TIME = 5000;    // 5 sec
-    private static final int NOTIFICATION_WAIT_TIME = 30000;
+    private static final int NOTIFICATION_WAIT_TIME = 60000;
 
     protected UiDevice mDevice;
     protected DPadUtil mDPadUtil;
+    private Instrumentation mInstrumentation;
 
 
     /**
@@ -195,7 +199,15 @@
     @Override
     public long launch(String appName, String packageName) {
         BySelector app = By.res(getSupportedLauncherPackage(), "app_banner").desc(appName);
-        return launchApp(this, app, packageName);
+        return launchApp(this, app, packageName, isGame(packageName));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void setInstrumentation(Instrumentation instrumentation) {
+        mInstrumentation = instrumentation;
     }
 
     /**
@@ -355,12 +367,12 @@
     }
 
     protected long launchApp(ILauncherStrategy launcherStrategy, BySelector app,
-            String packageName) {
-        return launchApp(launcherStrategy, app, packageName, MAX_SCROLL_ATTEMPTS);
+            String packageName, boolean isGame) {
+        return launchApp(launcherStrategy, app, packageName, isGame, MAX_SCROLL_ATTEMPTS);
     }
 
     protected long launchApp(ILauncherStrategy launcherStrategy, BySelector app,
-            String packageName, int maxScrollAttempts) {
+            String packageName, boolean isGame, int maxScrollAttempts) {
         unlockDeviceIfAsleep();
 
         if (isAppOpen(packageName)) {
@@ -370,14 +382,26 @@
 
         // Go to the home page
         launcherStrategy.open();
-        // attempt to find the app icon if it's not already on the screen
-        UiObject2 container = launcherStrategy.openAllApps(false);
+
+        // attempt to find the app/game icon if it's not already on the screen
+        UiObject2 container;
+        if (isGame) {
+            container = selectGamesRow();
+        } else {
+            container = launcherStrategy.openAllApps(false);
+        }
         UiObject2 appIcon = container.findObject(app);
         int attempts = 0;
         while (attempts++ < maxScrollAttempts) {
+            UiObject2 focused = container.wait(Until.findObject(By.focused(true)), SHORT_WAIT_TIME);
+            if (focused == null) {
+                throw new IllegalStateException(
+                        "The App/Game row may have lost focus while activity is in transition");
+            }
+
             // Compare the focused icon and the app icon to search for.
-            UiObject2 focusedIcon = container.findObject(By.focused(true))
-                    .findObject(By.res(getSupportedLauncherPackage(), "app_banner"));
+            UiObject2 focusedIcon = focused.findObject(
+                    By.res(getSupportedLauncherPackage(), "app_banner"));
 
             if (appIcon == null) {
                 appIcon = findApp(container, focusedIcon, app);
@@ -541,21 +565,54 @@
     }
 
     protected UiObject2 findNotificationCard(BySelector selector) {
-        // Move to the first notification, Search to the right
+        // Move to the first notification row, start searching to the right, then to the left
         mDPadUtil.pressHome();
+        UiObject2 card;
+        if ((card = findNotificationCard(selector, Direction.RIGHT)) != null) {
+            return card;
+        }
+        if ((card = findNotificationCard(selector, Direction.LEFT)) != null) {
+            return card;
+        }
+        return null;
+    }
 
-        // Find if a focused card matches a given selector
-        UiObject2 currentFocus = mDevice.findObject(getNotificationRowSelector())
-                .findObject(By.res(getSupportedLauncherPackage(), "card").focused(true));
+    /**
+     * Find the card in the Notification row that matches BySelector in a given direction.
+     * If a card is already selected, it returns regardless of the direction parameter.
+     * @param selector
+     * @param direction
+     * @return
+     */
+    protected UiObject2 findNotificationCard(BySelector selector, Direction direction) {
+        if (direction != Direction.RIGHT && direction != Direction.LEFT) {
+            throw new IllegalArgumentException("Required to go either left or right to find a card"
+                    + "in the Notification row");
+        }
+
+        // Find the Notification row
+        UiObject2 notification = mDevice.findObject(getNotificationRowSelector());
+        if (notification == null) {
+            mDPadUtil.pressHome();
+            notification = mDevice.wait(Until.findObject(getNotificationRowSelector()),
+                    SHORT_WAIT_TIME);
+            if (notification == null) {
+                throw new IllegalStateException("The Notification row is not found");
+            }
+        }
+
+        // Find a focused card in the Notification row that matches a given selector
+        UiObject2 currentFocus = notification.findObject(
+                By.res(getSupportedLauncherPackage(), "card").focused(true));
         UiObject2 previousFocus = null;
         while (!currentFocus.equals(previousFocus)) {
             if (currentFocus.hasObject(selector)) {
                 return currentFocus;   // Found
             }
-            mDPadUtil.pressDPadRight();
+            mDPadUtil.pressDPad(direction);
             previousFocus = currentFocus;
-            currentFocus = mDevice.findObject(getNotificationRowSelector())
-                    .findObject(By.res(getSupportedLauncherPackage(), "card").focused(true));
+            currentFocus = notification.findObject(
+                    By.res(getSupportedLauncherPackage(), "card").focused(true));
         }
         Log.d(LOG_TAG, "Failed to find the Notification card until it reaches the end.");
         return null;
@@ -604,7 +661,8 @@
             throw new IllegalArgumentException("Required to go either up or down to find rows");
         }
 
-        UiObject2 currentFocused = mDevice.findObject(By.focused(true));
+        UiObject2 currentFocused = mDevice.wait(Until.findObject(By.focused(true)),
+                SHORT_WAIT_TIME);
         UiObject2 prevFocused = null;
         while (!currentFocused.equals(prevFocused)) {
             UiObject2 rowObject = mDevice.findObject(row);
@@ -614,7 +672,7 @@
 
             mDPadUtil.pressDPad(direction);
             prevFocused = currentFocused;
-            currentFocused = mDevice.findObject(By.focused(true));
+            currentFocused = mDevice.wait(Until.findObject(By.focused(true)), SHORT_WAIT_TIME);
         }
         Log.d(LOG_TAG, "Failed to find the row until it reaches the end.");
         return null;
@@ -667,4 +725,25 @@
         Log.d(LOG_TAG, "Failed to find the setting in Settings row.");
         return null;
     }
+
+    private boolean isGame(String packageName) {
+        boolean isGame = false;
+        if (mInstrumentation != null) {
+            try {
+                ApplicationInfo appInfo =
+                        mInstrumentation.getTargetContext().getPackageManager().getApplicationInfo(
+                                packageName, 0);
+                // TV game apps should use the "isGame" tag added since the L release. They are
+                // listed on the Games row on the Leanback Launcher.
+                isGame = ((appInfo.flags & ApplicationInfo.FLAG_IS_GAME) != 0) ||
+                        (appInfo.metaData != null && appInfo.metaData.getBoolean("isGame", false));
+                Log.i(LOG_TAG, String.format("The package %s isGame: %b", packageName, isGame));
+            } catch (PackageManager.NameNotFoundException e) {
+                Log.w(LOG_TAG,
+                        String.format("No package found: %s, error:%s", packageName, e.toString()));
+                return false;
+            }
+        }
+        return isGame;
+    }
 }
diff --git a/libraries/launcher-helper/src/android/support/test/launcherhelper/TvLauncherStrategy.java b/libraries/launcher-helper/src/android/support/test/launcherhelper/TvLauncherStrategy.java
new file mode 100644
index 0000000..610fdbb
--- /dev/null
+++ b/libraries/launcher-helper/src/android/support/test/launcherhelper/TvLauncherStrategy.java
@@ -0,0 +1,633 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.test.launcherhelper;
+
+import android.app.Instrumentation;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.graphics.Point;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.platform.test.utils.DPadUtil;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.util.Log;
+import android.view.KeyEvent;
+
+import org.junit.Assert;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+
+public class TvLauncherStrategy implements ILeanbackLauncherStrategy {
+
+    private static final String LOG_TAG = TvLauncherStrategy.class.getSimpleName();
+    private static final String PACKAGE_LAUNCHER = "com.google.android.tvlauncher";
+
+    private static final int APP_LAUNCH_TIMEOUT = 10000;
+    private static final int SHORT_WAIT_TIME = 5000;    // 5 sec
+    private static final int UI_TRANSITION_WAIT_TIME = 1000;
+
+    // Note that the selector specifies criteria for matching an UI element from/to a focused item
+    private static final BySelector SELECTOR_TOP_ROW = By.res(PACKAGE_LAUNCHER, "top_row");
+    private static final BySelector SELECTOR_APPS_ROW = By.res(PACKAGE_LAUNCHER, "apps_row");
+    private static final BySelector SELECTOR_ALL_APPS_VIEW =
+            By.res(PACKAGE_LAUNCHER, "row_list_view");
+    private static final BySelector SELECTOR_ALL_APPS_LOGO =
+            By.res(PACKAGE_LAUNCHER, "channel_logo").focused(true).descContains("Apps");
+    private static final BySelector SELECTOR_CONFIG_CHANNELS_ROW =
+            By.res(PACKAGE_LAUNCHER, "configure_channels_row");
+
+    protected UiDevice mDevice;
+    protected DPadUtil mDPadUtil;
+    private Instrumentation mInstrumentation;
+
+    /**
+     * A TvLauncherUnsupportedOperationException is an exception specific to TV Launcher. This will
+     * be thrown when the feature/method is not available on the TV Launcher.
+     */
+    class TvLauncherUnsupportedOperationException extends UnsupportedOperationException {
+        TvLauncherUnsupportedOperationException() {
+            super();
+        }
+        TvLauncherUnsupportedOperationException(String msg) {
+            super(msg);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getSupportedLauncherPackage() {
+        return PACKAGE_LAUNCHER;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    // TODO(hyungtaekim): Move this common implementation to abstract class for TV launchers
+    @Override
+    public void setUiDevice(UiDevice uiDevice) {
+        mDevice = uiDevice;
+        mDPadUtil = new DPadUtil(mDevice);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void open() {
+        // if we see main list view, assume at home screen already
+        if (!mDevice.hasObject(getWorkspaceSelector())) {
+            mDPadUtil.pressHome();
+            // ensure launcher is shown
+            if (!mDevice.wait(Until.hasObject(getWorkspaceSelector()), SHORT_WAIT_TIME)) {
+                // HACK: dump hierarchy to logcat
+                ByteArrayOutputStream baos = new ByteArrayOutputStream();
+                try {
+                    mDevice.dumpWindowHierarchy(baos);
+                    baos.flush();
+                    baos.close();
+                    String[] lines = baos.toString().split("\\r?\\n");
+                    for (String line : lines) {
+                        Log.d(LOG_TAG, line.trim());
+                    }
+                } catch (IOException ioe) {
+                    Log.e(LOG_TAG, "error dumping XML to logcat", ioe);
+                }
+                throw new RuntimeException("Failed to open TV launcher");
+            }
+            mDevice.waitForIdle();
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     * There are two different ways to open All Apps view. If longpress is true, it will long press
+     * the HOME key to open it. Otherwise it will navigate to the "APPS" logo on the Apps row.
+     */
+    @Override
+    public UiObject2 openAllApps(boolean longpress) {
+        if (longpress) {
+            mDPadUtil.longPressKeyCode(KeyEvent.KEYCODE_HOME);
+        } else {
+            Assert.assertNotNull("Could not find all apps logo", selectAppsLogo());
+            mDPadUtil.pressDPadCenter();
+        }
+        return mDevice.wait(Until.findObject(getAllAppsSelector()), SHORT_WAIT_TIME);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public BySelector getWorkspaceSelector() {
+        return By.res(getSupportedLauncherPackage(), "home_view_container");
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public BySelector getSearchRowSelector() {
+        return  SELECTOR_TOP_ROW;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public BySelector getAppsRowSelector() {
+        return SELECTOR_APPS_ROW;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public BySelector getGamesRowSelector() {
+        // Note that the apps and games are now in the same row on new TV Launcher.
+        return getAppsRowSelector();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public Direction getAllAppsScrollDirection() {
+        return Direction.DOWN;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public BySelector getAllAppsSelector() {
+        return SELECTOR_ALL_APPS_VIEW;
+    }
+
+    public BySelector getAllAppsLogoSelector() {
+        return SELECTOR_ALL_APPS_LOGO;
+    }
+
+    /**
+     * Returns a {@link BySelector} describing a given favorite app
+     */
+    public BySelector getFavoriteAppSelector(String appName) {
+        return By.res(getSupportedLauncherPackage(), "favorite_app_banner").text(appName);
+    }
+
+    /**
+     * Returns a {@link BySelector} describing a given app in Apps View
+     */
+    public BySelector getAppInAppsViewSelector(String appName) {
+        return By.res(getSupportedLauncherPackage(), "app_title").text(appName);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public long launch(String appName, String packageName) {
+        return launchApp(this, appName, packageName, isGame(packageName));
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * This function must be called before any UI test runs on TV.
+     * </p>
+     */
+    @Override
+    public void setInstrumentation(Instrumentation instrumentation) {
+        mInstrumentation = instrumentation;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public UiObject2 selectSearchRow() {
+        // The Search orb is now on top row on TV Launcher
+        return selectTopRow();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public UiObject2 selectAppsRow() {
+        return selectBidirect(getAppsRowSelector().hasDescendant(By.focused(true)),
+                Direction.DOWN);
+    }
+
+    public UiObject2 selectChannelsRow(String channelName) {
+        // TODO:
+        return null;
+    }
+
+    public UiObject2 selectAppsLogo() {
+        Assert.assertNotNull("Could not find all apps row", selectAppsRow());
+        return selectBidirect(getAllAppsLogoSelector().hasDescendant(By.focused(true)),
+                Direction.LEFT);
+    }
+
+    /**
+     * Returns a {@link UiObject2} describing the Top row on TV Launcher
+     * @return
+     */
+    public UiObject2 selectTopRow() {
+        return select(getSearchRowSelector().hasDescendant(By.focused(true)),
+                Direction.UP, UI_TRANSITION_WAIT_TIME);
+    }
+
+    /**
+     * Returns a {@link UiObject2} describing the Config Channels row on TV Launcher
+     * @return
+     */
+    public UiObject2 selectConfigChannelsRow() {
+        return select(SELECTOR_CONFIG_CHANNELS_ROW.hasDescendant(By.focused(true)),
+                Direction.DOWN, UI_TRANSITION_WAIT_TIME);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public UiObject2 selectGamesRow() {
+        return selectAppsRow();
+    }
+
+    /**
+     * Select the given app in All Apps activity.
+     * When the All Apps opens, the focus is always at the top right.
+     * Search from left to right, and down to the next row, from right to left, and
+     * down to the next row like a zigzag pattern until the app is found.
+     */
+    protected UiObject2 selectAppInAllApps(BySelector appSelector, String packageName) {
+        openAllApps(true);
+
+        // Assume that the focus always starts at the top left of the Apps view.
+        final int maxScrollAttempts = 20;
+        final int margin = 10;
+        int attempts = 0;
+        UiObject2 focused = null;
+        UiObject2 expected = null;
+        while (attempts++ < maxScrollAttempts) {
+            focused = mDevice.wait(Until.findObject(By.focused(true)), SHORT_WAIT_TIME);
+            expected = mDevice.wait(Until.findObject(appSelector), SHORT_WAIT_TIME);
+
+            if (expected == null) {
+                mDPadUtil.pressDPadDown();
+                continue;
+            } else if (focused.getVisibleCenter().equals(expected.getVisibleCenter())) {
+                // The app icon is on the screen, and selected.
+                Log.i(LOG_TAG, String.format("The app %s is selected", packageName));
+                break;
+            } else {
+                // The app icon is on the screen, but not selected yet
+                // Move one step closer to the app icon
+                Point currentPosition = focused.getVisibleCenter();
+                Point targetPosition = expected.getVisibleCenter();
+                int dx = targetPosition.x - currentPosition.x;
+                int dy = targetPosition.y - currentPosition.y;
+                if (dy > margin) {
+                    mDPadUtil.pressDPadDown();
+                    continue;
+                }
+                if (dx > margin) {
+                    mDPadUtil.pressDPadRight();
+                    continue;
+                }
+                if (dy < -margin) {
+                    mDPadUtil.pressDPadUp();
+                    continue;
+                }
+                if (dx < -margin) {
+                    mDPadUtil.pressDPadLeft();
+                    continue;
+                }
+                throw new RuntimeException(
+                        "Failed to navigate to the app icon on screen: " + packageName);
+            }
+        }
+        return expected;
+    }
+
+    /**
+     * Select the given app in All Apps activity in zigzag manner.
+     * When the All Apps opens, the focus is always at the top left.
+     * Search from left to right, and down to the next row, from right to left, and
+     * down to the next row like a zigzag pattern until it founds a given app.
+     */
+    public UiObject2 selectAppInAllAppsZigZag(BySelector appSelector, String packageName) {
+        Direction direction = Direction.RIGHT;
+        UiObject2 app = select(appSelector, direction, UI_TRANSITION_WAIT_TIME);
+        while (app == null && move(Direction.DOWN)) {
+            direction = Direction.reverse(direction);
+            app = select(appSelector, direction, UI_TRANSITION_WAIT_TIME);
+        }
+        if (app != null) {
+            Log.i(LOG_TAG, String.format("The app %s is selected", packageName));
+        }
+        return app;
+    }
+
+    protected long launchApp(ILauncherStrategy launcherStrategy, String appName,
+            String packageName, boolean isGame) {
+        unlockDeviceIfAsleep();
+
+        if (isAppOpen(packageName)) {
+            // Application is already open
+            return 0;
+        }
+
+        // Go to the home page, and select the Apps row
+        launcherStrategy.open();
+        selectAppsRow();
+
+        // Search for the app in the Favorite Apps row first.
+        // If not exists, open the 'All Apps' and search for the app there
+        UiObject2 app = null;
+        BySelector favAppSelector = getFavoriteAppSelector(appName);
+        if (mDevice.hasObject(favAppSelector)) {
+            app = selectBidirect(By.focused(true).hasDescendant(favAppSelector), Direction.RIGHT);
+        } else {
+            openAllApps(true);
+            // Find app in Apps View in zigzag mode with app selector for Apps View
+            // because the app title no longer appears until focused.
+            app = selectAppInAllAppsZigZag(getAppInAppsViewSelector(appName), packageName);
+        }
+        if (app == null) {
+            throw new RuntimeException(
+                    "Failed to navigate to the app icon on screen: " + packageName);
+        }
+
+        // The app icon is already found and focused. Then wait for it to open.
+        long ready = SystemClock.uptimeMillis();
+        mDPadUtil.pressDPadCenter();
+        if (packageName != null) {
+            if (!mDevice.wait(Until.hasObject(By.pkg(packageName).depth(0)), APP_LAUNCH_TIMEOUT)) {
+                Log.w(LOG_TAG, String.format(
+                    "No UI element with package name %s detected.", packageName));
+                return ILauncherStrategy.LAUNCH_FAILED_TIMESTAMP;
+            }
+        }
+        return ready;
+    }
+
+    protected boolean isTopRowSelected() {
+        UiObject2 row = mDevice.findObject(getSearchRowSelector());
+        if (row == null) {
+            return false;
+        }
+        return row.hasObject(By.focused(true));
+    }
+
+    protected boolean isAppsRowSelected() {
+        UiObject2 row = mDevice.findObject(getAppsRowSelector());
+        if (row == null) {
+            return false;
+        }
+        return row.hasObject(By.focused(true));
+    }
+
+    protected boolean isGamesRowSelected() {
+        return isAppsRowSelected();
+    }
+
+    // TODO(hyungtaekim): Move in the common helper
+    protected boolean isAppOpen(String appPackage) {
+        return mDevice.hasObject(By.pkg(appPackage).depth(0));
+    }
+
+    // TODO(hyungtaekim): Move in the common helper
+    protected void unlockDeviceIfAsleep() {
+        // Turn screen on if necessary
+        try {
+            if (!mDevice.isScreenOn()) {
+                mDevice.wakeUp();
+            }
+        } catch (RemoteException e) {
+            Log.e(LOG_TAG, "Failed to unlock the screen-off device.", e);
+        }
+    }
+
+    private boolean isGame(String packageName) {
+        boolean isGame = false;
+        if (mInstrumentation != null) {
+            try {
+                ApplicationInfo appInfo =
+                        mInstrumentation.getTargetContext().getPackageManager().getApplicationInfo(
+                                packageName, 0);
+                // TV game apps should use the "isGame" tag added since the L release. They are
+                // listed on the Games row on the TV Launcher.
+                isGame = (appInfo.metaData != null && appInfo.metaData.getBoolean("isGame", false))
+                        || ((appInfo.flags & ApplicationInfo.FLAG_IS_GAME) != 0);
+                Log.i(LOG_TAG, String.format("The package %s isGame: %b", packageName, isGame));
+            } catch (PackageManager.NameNotFoundException e) {
+                Log.w(LOG_TAG,
+                        String.format("No package found: %s, error:%s", packageName, e.toString()));
+                return false;
+            }
+        }
+        return isGame;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void search(String query) {
+        // TODO: Implement this method when the feature is available
+        throw new UnsupportedOperationException("search is not yet implemented");
+    }
+
+    public void selectRestrictedProfile() {
+        // TODO: Implement this method when the feature is available
+        throw new UnsupportedOperationException(
+                "The Restricted Profile is not yet available on TV Launcher.");
+    }
+
+
+    // Convenient methods for UI actions
+
+    /**
+     * Select an UI element with given {@link BySelector}. This action keeps moving a focus
+     * in a given {@link Direction} until it finds a matched element.
+     * @param selector the search criteria to match an element
+     * @param direction the direction to find
+     * @param timeoutMs timeout in milliseconds to select
+     * @return a UiObject2 which represents the matched element
+     */
+    public UiObject2 select(BySelector selector, Direction direction, long timeoutMs) {
+        UiObject2 focus = mDevice.wait(Until.findObject(By.focused(true)), SHORT_WAIT_TIME);
+        while (!mDevice.wait(Until.hasObject(selector), timeoutMs)) {
+            Log.d(LOG_TAG, String.format("select: moving a focus from %s to %s", focus, direction));
+            UiObject2 focused = focus;
+            mDPadUtil.pressDPad(direction);
+            focus = mDevice.wait(Until.findObject(By.focused(true)), SHORT_WAIT_TIME);
+            // Hack: A focus might be lost in some UI. Take one more step forward.
+            if (focus == null) {
+                mDPadUtil.pressDPad(direction);
+                focus = mDevice.wait(Until.findObject(By.focused(true)), SHORT_WAIT_TIME);
+            }
+            // Check if it reaches to an end where it no longer moves a focus to next element
+            if (focused.equals(focus)) {
+                Log.d(LOG_TAG, "select: not found until it reaches to an end.");
+                return null;
+            }
+        }
+        Log.i(LOG_TAG, String.format("select: %s is selected", focus));
+        return focus;
+    }
+
+    /**
+     * Select an element with a given {@link BySelector} in both given direction and reverse.
+     */
+    public UiObject2 selectBidirect(BySelector selector, Direction direction) {
+        Log.d(LOG_TAG, String.format("selectBidirect [direction]%s", direction));
+        UiObject2 object = select(selector, direction, UI_TRANSITION_WAIT_TIME);
+        if (object == null) {
+            object = select(selector, Direction.reverse(direction), UI_TRANSITION_WAIT_TIME);
+        }
+        return object;
+    }
+
+    /**
+     * Simulate a move pressing a key code.
+     * Return true if a focus is shifted on TV UI, otherwise false.
+     */
+    public boolean move(Direction direction) {
+        int keyCode = KeyEvent.KEYCODE_UNKNOWN;
+        switch (direction) {
+            case LEFT:
+                keyCode = KeyEvent.KEYCODE_DPAD_LEFT;
+                break;
+            case RIGHT:
+                keyCode = KeyEvent.KEYCODE_DPAD_RIGHT;
+                break;
+            case UP:
+                keyCode = KeyEvent.KEYCODE_DPAD_UP;
+                break;
+            case DOWN:
+                keyCode = KeyEvent.KEYCODE_DPAD_DOWN;
+                break;
+            default:
+                throw new RuntimeException(String.format("This direction %s is not supported.",
+                    direction));
+        }
+        UiObject2 focus = mDevice.wait(Until.findObject(By.focused(true)),
+                UI_TRANSITION_WAIT_TIME);
+        mDPadUtil.pressKeyCodeAndWait(keyCode);
+        return !focus.equals(mDevice.wait(Until.findObject(By.focused(true)),
+                UI_TRANSITION_WAIT_TIME));
+    }
+
+
+    // Unsupported methods
+
+    @SuppressWarnings("unused")
+    @Override
+    public BySelector getNotificationRowSelector() {
+        throw new TvLauncherUnsupportedOperationException("No Notification row");
+    }
+
+    @SuppressWarnings("unused")
+    @Override
+    public BySelector getSettingsRowSelector() {
+        throw new TvLauncherUnsupportedOperationException("No Settings row");
+    }
+
+    @SuppressWarnings("unused")
+    @Override
+    public BySelector getAppWidgetSelector() {
+        throw new TvLauncherUnsupportedOperationException();
+    }
+
+    @SuppressWarnings("unused")
+    @Override
+    public BySelector getNowPlayingCardSelector() {
+        throw new TvLauncherUnsupportedOperationException("No Now Playing Card");
+    }
+
+    @SuppressWarnings("unused")
+    @Override
+    public UiObject2 selectNotificationRow() {
+        throw new TvLauncherUnsupportedOperationException("No Notification row");
+    }
+
+    @SuppressWarnings("unused")
+    @Override
+    public UiObject2 selectSettingsRow() {
+        throw new TvLauncherUnsupportedOperationException("No Settings row");
+    }
+
+    @SuppressWarnings("unused")
+    @Override
+    public boolean hasAppWidgetSelector() {
+        throw new TvLauncherUnsupportedOperationException();
+    }
+
+    @SuppressWarnings("unused")
+    @Override
+    public boolean hasNowPlayingCard() {
+        throw new TvLauncherUnsupportedOperationException("No Now Playing Card");
+    }
+
+    @SuppressWarnings("unused")
+    @Override
+    public BySelector getAllAppsButtonSelector() {
+        throw new TvLauncherUnsupportedOperationException("No All Apps button");
+    }
+
+    @SuppressWarnings("unused")
+    @Override
+    public UiObject2 openAllWidgets(boolean reset) {
+        throw new TvLauncherUnsupportedOperationException("No All Widgets");
+    }
+
+    @SuppressWarnings("unused")
+    @Override
+    public BySelector getAllWidgetsSelector() {
+        throw new TvLauncherUnsupportedOperationException("No All Widgets");
+    }
+
+    @SuppressWarnings("unused")
+    @Override
+    public Direction getAllWidgetsScrollDirection() {
+        throw new TvLauncherUnsupportedOperationException("No All Widgets");
+    }
+
+    @SuppressWarnings("unused")
+    @Override
+    public BySelector getHotSeatSelector() {
+        throw new TvLauncherUnsupportedOperationException("No Hot seat");
+    }
+
+    @SuppressWarnings("unused")
+    @Override
+    public Direction getWorkspaceScrollDirection() {
+        throw new TvLauncherUnsupportedOperationException("No Workspace");
+    }
+}
diff --git a/libraries/launcher-helper/src/android/support/test/launcherhelper/WearLauncherStrategy.java b/libraries/launcher-helper/src/android/support/test/launcherhelper/WearLauncherStrategy.java
index 7f326ab..7f913ad 100644
--- a/libraries/launcher-helper/src/android/support/test/launcherhelper/WearLauncherStrategy.java
+++ b/libraries/launcher-helper/src/android/support/test/launcherhelper/WearLauncherStrategy.java
@@ -63,21 +63,9 @@
     @Override
     public void open() {
         if (!mDevice.hasObject(getHotSeatSelector())) {
-            mDevice.pressBack();
+            mDevice.pressHome();
             if (!mDevice.wait(Until.hasObject(getHotSeatSelector()), 5000)) {
-                // HACK: dump hierarchy to logcat
-                ByteArrayOutputStream baos = new ByteArrayOutputStream();
-                try {
-                    mDevice.dumpWindowHierarchy(baos);
-                    baos.flush();
-                    baos.close();
-                    String[] lines = baos.toString().split("\\r?\\n");
-                    for (String line : lines) {
-                        Log.d(LOG_TAG, line.trim());
-                    }
-                } catch (IOException ioe) {
-                    Log.e(LOG_TAG, "error dumping XML to logcat", ioe);
-                }
+                dumpScreen("Return Watch face");
                 Assert.fail("Failed to open launcher");
             }
             mDevice.waitForIdle();
@@ -92,13 +80,21 @@
     @Override
     public UiObject2 openAllApps(boolean reset) {
         if (!mDevice.hasObject(getAllAppsSelector())) {
-            mDevice.pressBack();
+            mDevice.pressHome();
+            mDevice.waitForIdle();
+            if (!mDevice.wait(Until.hasObject(getAllAppsSelector()), 5000)) {
+                dumpScreen("Open launcher");
+                Assert.fail("Failed to open launcher");
+            }
         }
         UiObject2 allAppsContainer = mDevice.wait(Until.findObject(getAllAppsSelector()), 2000);
         if (reset) {
             CommonLauncherHelper.getInstance(mDevice).scrollBackToBeginning(
                     allAppsContainer, Direction.reverse(getAllAppsScrollDirection()));
         }
+        if (allAppsContainer == null) {
+            Assert.fail("Failed to find launcher");
+        }
         return allAppsContainer;
     }
 
@@ -180,4 +176,19 @@
                 getSupportedLauncherPackage(), "title").clazz(TextView.class).text(appName);
         return CommonLauncherHelper.getInstance(mDevice).launchApp(this, app, packageName);
     }
+
+    private void dumpScreen(String description) {
+        // DEBUG: dump hierarchy to logcat
+        Log.d(LOG_TAG, "Dump Screen at " + description);
+        try (ByteArrayOutputStream baos = new ByteArrayOutputStream()){
+            mDevice.dumpWindowHierarchy(baos);
+            baos.flush();
+            String[] lines = baos.toString().split(System.lineSeparator());
+            for (String line : lines) {
+                Log.d(LOG_TAG, line.trim());
+            }
+        } catch (IOException ioe) {
+            Log.e(LOG_TAG, "error dumping XML to logcat", ioe);
+        }
+    }
 }
diff --git a/libraries/base-app-helpers/Android.mk b/libraries/metrics-helper/Android.mk
similarity index 82%
copy from libraries/base-app-helpers/Android.mk
copy to libraries/metrics-helper/Android.mk
index 0111a0a..3246485 100644
--- a/libraries/base-app-helpers/Android.mk
+++ b/libraries/metrics-helper/Android.mk
@@ -13,12 +13,15 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 #
+
 LOCAL_PATH := $(call my-dir)
 
 include $(CLEAR_VARS)
-LOCAL_MODULE := base-app-helpers
-LOCAL_STATIC_JAVA_LIBRARIES := dpad-util
-LOCAL_JAVA_LIBRARIES := ub-uiautomator launcher-helper-lib
+
+LOCAL_MODULE := metrics-helper-lib
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_STATIC_JAVA_LIBRARIES := services.core legacy-android-test junit
 
 include $(BUILD_STATIC_JAVA_LIBRARY)
+
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/libraries/metrics-helper/src/android/support/test/metricshelper/MetricsAsserts.java b/libraries/metrics-helper/src/android/support/test/metricshelper/MetricsAsserts.java
new file mode 100644
index 0000000..613f2e0
--- /dev/null
+++ b/libraries/metrics-helper/src/android/support/test/metricshelper/MetricsAsserts.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.test.metricshelper;
+
+import static junit.framework.Assert.assertTrue;
+
+import android.metrics.LogMaker;
+import android.metrics.MetricsReader;
+
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.Queue;
+
+/**
+ * Useful test utilities for metrics tests.
+ */
+public class MetricsAsserts {
+
+    /**
+     * Assert unless there is a log with the matching category and with ACTION type.
+     */
+    public static void assertHasActionLog(String message, MetricsReader reader, int view) {
+        reader.read(0);
+        assertHasActionLog(message, new ReaderQueue(reader), view);
+    }
+    /**
+     * Assert unless there is a log with the matching category and with ACTION type.
+     */
+    public static void assertHasActionLog(String message, Queue<LogMaker> queue, int view) {
+        Queue<LogMaker> logs = findMatchingLogs(queue,
+                new LogMaker(view)
+                        .setType(MetricsEvent.TYPE_ACTION));
+        assertTrue(message, !logs.isEmpty());
+    }
+
+    /**
+     * Assert unless there is a log with the matching category and with visibility type.
+     */
+    public static void assertHasVisibilityLog(String message, MetricsReader reader,
+            int view, boolean visible) {
+        reader.read(0);
+        assertHasVisibilityLog(message, new ReaderQueue(reader), view, visible);
+    }
+
+    /**
+     * Assert unless there is a log with the matching category and with visibility type.
+     */
+    public static void assertHasVisibilityLog(String message, Queue<LogMaker> queue,
+            int view, boolean visible) {
+        Queue<LogMaker> logs = findMatchingLogs(queue,
+                new LogMaker(view)
+                        .setType(visible ? MetricsEvent.TYPE_OPEN : MetricsEvent.TYPE_CLOSE));
+        assertTrue(message, !logs.isEmpty());
+    }
+
+    /**
+     * @returns logs that have at least all the matching fields in the template.
+     */
+    public static Queue<LogMaker> findMatchingLogs(MetricsReader reader, LogMaker template) {
+        reader.read(0);
+        return findMatchingLogs(new ReaderQueue(reader), template);
+    }
+
+    /**
+     * @returns logs that have at least all the matching fields in the template.
+     */
+    public static Queue<LogMaker> findMatchingLogs(Queue<LogMaker> queue, LogMaker template) {
+        LinkedList<LogMaker> logs = new LinkedList<>();
+        if (template == null) {
+            return logs;
+        }
+        while (!queue.isEmpty()) {
+            LogMaker b = queue.poll();
+            if (template.isSubsetOf(b)) {
+                logs.push(b);
+            }
+        }
+        return logs;
+    }
+
+    /**
+     * Assert unless there is at least one  log that matches the template.
+     */
+    public static void assertHasLog(String message, MetricsReader reader, LogMaker expected) {
+        reader.read(0);
+        assertHasLog(message, new ReaderQueue(reader), expected);
+    }
+
+    /**
+     * Assert unless there is at least one  log that matches the template.
+     */
+    public static void assertHasLog(String message, Queue<LogMaker> queue, LogMaker expected) {
+        assertTrue(message, !findMatchingLogs(queue, expected).isEmpty());
+    }
+
+    private static class ReaderQueue implements Queue<LogMaker> {
+
+        private final MetricsReader mMetricsReader;
+
+        ReaderQueue(MetricsReader metricsReader) {
+            mMetricsReader = metricsReader;
+        }
+
+        @Override
+        public boolean isEmpty() {
+            return !mMetricsReader.hasNext();
+        }
+
+        @Override
+        public LogMaker poll() {
+            return mMetricsReader.next();
+        }
+
+        @Override
+        public boolean add(LogMaker logMaker) {
+            throw new UnsupportedOperationException("unimplemented fake method");
+        }
+
+        @Override
+        public boolean addAll(Collection<? extends LogMaker> collection) {
+            throw new UnsupportedOperationException("unimplemented fake method");
+        }
+
+        @Override
+        public void clear() {
+            throw new UnsupportedOperationException("unimplemented fake method");
+        }
+
+        @Override
+        public boolean contains(Object object) {
+            throw new UnsupportedOperationException("unimplemented fake method");
+        }
+
+        @Override
+        public boolean containsAll(Collection<?> collection) {
+            throw new UnsupportedOperationException("unimplemented fake method");
+        }
+
+        @Override
+        public Iterator<LogMaker> iterator() {
+            throw new UnsupportedOperationException("unimplemented fake method");
+        }
+
+        @Override
+        public boolean remove(Object object) {
+            throw new UnsupportedOperationException("unimplemented fake method");
+        }
+
+        @Override
+        public boolean removeAll(Collection<?> collection) {
+            throw new UnsupportedOperationException("unimplemented fake method");
+        }
+
+        @Override
+        public boolean retainAll(Collection<?> collection) {
+            throw new UnsupportedOperationException("unimplemented fake method");
+        }
+
+        @Override
+        public int size() {
+            throw new UnsupportedOperationException("unimplemented fake method");
+        }
+
+        @Override
+        public Object[] toArray() {
+            throw new UnsupportedOperationException("unimplemented fake method");
+        }
+
+        @Override
+        public <T> T[] toArray(T[] array) {
+            throw new UnsupportedOperationException("unimplemented fake method");
+        }
+
+        @Override
+        public boolean offer(LogMaker logMaker) {
+            throw new UnsupportedOperationException("unimplemented fake method");
+        }
+
+        @Override
+        public LogMaker remove() {
+            throw new UnsupportedOperationException("unimplemented fake method");
+        }
+
+        @Override
+        public LogMaker element() {
+            throw new UnsupportedOperationException("unimplemented fake method");
+        }
+
+        @Override
+        public LogMaker peek() {
+            throw new UnsupportedOperationException("unimplemented fake method");
+        }
+    }
+}
diff --git a/libraries/base-app-helpers/Android.mk b/libraries/metrics-helper/tests/Android.mk
similarity index 63%
copy from libraries/base-app-helpers/Android.mk
copy to libraries/metrics-helper/tests/Android.mk
index 0111a0a..00567d6 100644
--- a/libraries/base-app-helpers/Android.mk
+++ b/libraries/metrics-helper/tests/Android.mk
@@ -1,5 +1,4 @@
-#
-# Copyright (C) 2016 The Android Open Source Project
+# Copyright (C) 2017 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -12,13 +11,23 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-#
-LOCAL_PATH := $(call my-dir)
 
+LOCAL_PATH := $(call my-dir)
 include $(CLEAR_VARS)
-LOCAL_MODULE := base-app-helpers
-LOCAL_STATIC_JAVA_LIBRARIES := dpad-util
-LOCAL_JAVA_LIBRARIES := ub-uiautomator launcher-helper-lib
+
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_PACKAGE_NAME := MetricsHelperTests
+
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
-include $(BUILD_STATIC_JAVA_LIBRARY)
+LOCAL_JAVA_LIBRARIES := android.test.runner
+LOCAL_STATIC_JAVA_LIBRARIES := \
+    android-support-test \
+    mockito-target-minus-junit4 \
+    platform-test-annotations \
+    metrics-helper-lib \
+    framework-protos
+
+include $(BUILD_PACKAGE)
+
diff --git a/libraries/metrics-helper/tests/AndroidManifest.xml b/libraries/metrics-helper/tests/AndroidManifest.xml
new file mode 100644
index 0000000..7b1bf5d
--- /dev/null
+++ b/libraries/metrics-helper/tests/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT 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="android.support.test.metricshelper">
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="android.support.test.runner.AndroidJUnitRunner"
+        android:targetPackage="android.support.test.metricshelper"
+        android:label="Metrics Helper Tests" />
+</manifest>
diff --git a/libraries/metrics-helper/tests/src/android/support/test/metricshelper/MetricsAssertsTest.java b/libraries/metrics-helper/tests/src/android/support/test/metricshelper/MetricsAssertsTest.java
new file mode 100644
index 0000000..7dc5b66
--- /dev/null
+++ b/libraries/metrics-helper/tests/src/android/support/test/metricshelper/MetricsAssertsTest.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.test.metricshelper;
+
+import android.metrics.LogMaker;
+import android.metrics.MetricsReader;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+
+import android.test.suitebuilder.annotation.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import android.support.test.runner.AndroidJUnit4;
+
+import static junit.framework.Assert.assertEquals;
+import static org.mockito.Mockito.when;
+import static org.junit.Assert.fail;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class MetricsAssertsTest {
+    @Mock MetricsReader mReader;
+
+    private LogMaker a;
+    private LogMaker b;
+    private LogMaker c;
+    private LogMaker d;
+
+    private int mActionView = MetricsEvent.ACTION_WIFI_ON;
+    private int mOpenView = MetricsEvent.MAIN_SETTINGS;
+    private int mCloseView = MetricsEvent.NOTIFICATION_PANEL;
+    private int mSubtype = 4;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        a = new LogMaker(MetricsEvent.SCREEN)
+                .setType(MetricsEvent.TYPE_OPEN)
+                .setTimestamp(1000);
+        b = new LogMaker(mOpenView)
+                .setType(MetricsEvent.TYPE_OPEN)
+                .setTimestamp(2000);
+        c = new LogMaker(mActionView)
+                .setType(MetricsEvent.TYPE_ACTION)
+                .setSubtype(mSubtype)
+                .setTimestamp(3000);
+        d = new LogMaker(mCloseView)
+                .setType(MetricsEvent.TYPE_CLOSE)
+                .setTimestamp(4000);
+
+        when(mReader.hasNext())
+            .thenReturn(true)
+            .thenReturn(true)
+            .thenReturn(true)
+            .thenReturn(true)
+            .thenReturn(false);
+        when(mReader.next())
+            .thenReturn(a)
+            .thenReturn(b)
+            .thenReturn(c)
+            .thenReturn(d)
+            .thenReturn(null);
+    }
+
+    @Test
+    public void testHasActionLogTrue() {
+        MetricsAsserts.assertHasActionLog("foo", mReader, mActionView);
+    }
+
+    @Test
+    public void testHasActionLogFalse() {
+        final String message = "foo";
+        try {
+            MetricsAsserts.assertHasActionLog(message, mReader, mOpenView);
+        } catch (AssertionError e) {
+            assertEquals(message, e.getMessage());
+            return; // success!
+        }
+    }
+
+    @Test
+    public void testHasVisibileLogTrue() {
+        MetricsAsserts.assertHasVisibilityLog("foo", mReader, mOpenView, true);
+    }
+
+    @Test
+    public void testHasVisibleLogFalse() {
+        final String message = "foo";
+        try {
+            MetricsAsserts.assertHasVisibilityLog(message, mReader, mActionView, true);
+        } catch (AssertionError e) {
+            assertEquals(message, e.getMessage());
+            return; // success!
+        }
+    }
+
+    @Test
+    public void testHasHiddenLogTrue() {
+        MetricsAsserts.assertHasVisibilityLog("foo", mReader, mCloseView, false);
+    }
+
+    @Test
+    public void testHasHiddenLogFalse() {
+        final String message = "foo";
+        try {
+            MetricsAsserts.assertHasVisibilityLog(message, mReader, mOpenView, false);
+        } catch (AssertionError e) {
+            assertEquals(message, e.getMessage());
+            return; // success!
+        }
+    }
+
+    @Test
+    public void testHasTemplateLogCategoryOnly() {
+        MetricsAsserts.assertHasLog("didn't find existing log", mReader,
+                new LogMaker(mActionView));
+    }
+
+    @Test
+    public void testHasTemplateLogCategoryAndType() {
+        MetricsAsserts.assertHasLog("didn't find existing log", mReader,
+                new LogMaker(mActionView)
+                        .setType(MetricsEvent.TYPE_ACTION));
+    }
+
+    @Test
+    public void testHasTemplateLogCategoryTypeAndSubtype() {
+        MetricsAsserts.assertHasLog("didn't find existing log", mReader,
+                new LogMaker(mActionView)
+                        .setType(MetricsEvent.TYPE_ACTION)
+                        .setSubtype(mSubtype));
+    }
+
+    @Test
+    public void testDoesNotHaveTemplateLog() {
+        final String message = "foo";
+        try {
+            MetricsAsserts.assertHasLog(message, mReader,
+                    new LogMaker(mActionView)
+                            .setType(MetricsEvent.TYPE_ACTION)
+                            .setSubtype(mSubtype));
+        } catch (AssertionError e) {
+            assertEquals(message, e.getMessage());
+            return; // success!
+        }
+
+    }
+}
diff --git a/libraries/base-app-helpers/Android.mk b/libraries/system-helpers/Android.mk
similarity index 62%
copy from libraries/base-app-helpers/Android.mk
copy to libraries/system-helpers/Android.mk
index 0111a0a..d2de885 100644
--- a/libraries/base-app-helpers/Android.mk
+++ b/libraries/system-helpers/Android.mk
@@ -16,9 +16,23 @@
 LOCAL_PATH := $(call my-dir)
 
 include $(CLEAR_VARS)
-LOCAL_MODULE := base-app-helpers
-LOCAL_STATIC_JAVA_LIBRARIES := dpad-util
-LOCAL_JAVA_LIBRARIES := ub-uiautomator launcher-helper-lib
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_MODULE := system-helpers
+LOCAL_STATIC_JAVA_LIBRARIES := account-helper \
+    activity-helper \
+    commands-helper \
+    connectivity-helper \
+    device-helper \
+    permission-helper \
+    settings-helper \
+    sysui-helper \
+    user-helper \
+    package-helper \
+    accessibility-helper
+LOCAL_SDK_VERSION := current
 
 include $(BUILD_STATIC_JAVA_LIBRARY)
+
+######################################
+
+include $(call all-makefiles-under, $(LOCAL_PATH))
diff --git a/libraries/base-app-helpers/Android.mk b/libraries/system-helpers/accessibility-helper/Android.mk
similarity index 75%
copy from libraries/base-app-helpers/Android.mk
copy to libraries/system-helpers/accessibility-helper/Android.mk
index 0111a0a..185fe2a 100644
--- a/libraries/base-app-helpers/Android.mk
+++ b/libraries/system-helpers/accessibility-helper/Android.mk
@@ -1,5 +1,5 @@
 #
-# Copyright (C) 2016 The Android Open Source Project
+# Copyright (C) 2017 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -16,9 +16,12 @@
 LOCAL_PATH := $(call my-dir)
 
 include $(CLEAR_VARS)
-LOCAL_MODULE := base-app-helpers
-LOCAL_STATIC_JAVA_LIBRARIES := dpad-util
-LOCAL_JAVA_LIBRARIES := ub-uiautomator launcher-helper-lib
+LOCAL_MODULE := accessibility-helper
+LOCAL_JAVA_LIBRARIES := android-support-test \
+    ub-uiautomator \
+    settings-helper \
+    package-helper \
+    activity-helper
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
-include $(BUILD_STATIC_JAVA_LIBRARY)
+include $(BUILD_STATIC_JAVA_LIBRARY)
\ No newline at end of file
diff --git a/libraries/system-helpers/accessibility-helper/src/android/system/helpers/AccessibilityHelper.java b/libraries/system-helpers/accessibility-helper/src/android/system/helpers/AccessibilityHelper.java
new file mode 100644
index 0000000..c14d2e4
--- /dev/null
+++ b/libraries/system-helpers/accessibility-helper/src/android/system/helpers/AccessibilityHelper.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.system.helpers;
+
+import android.app.Instrumentation;
+import android.content.Context;
+import android.provider.Settings;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.UiObjectNotFoundException;
+import android.support.test.uiautomator.UiScrollable;
+import android.support.test.uiautomator.UiSelector;
+import android.support.test.uiautomator.Until;
+
+/**
+ * Implement common helper functions for accessibility.
+ */
+public class AccessibilityHelper {
+    public static final String SETTINGS_PACKAGE = "com.android.settings";
+    public static final String BUTTON = "android.widget.Button";
+    public static final String CHECK_BOX = "android.widget.CheckBox";
+    public static final String IMAGE_BUTTON = "android.widget.ImageButton";
+    public static final String TEXT_VIEW = "android.widget.TextView";
+    public static final String SWITCH = "android.widget.Switch";
+    public static final String CHECKED_TEXT_VIEW = "android.widget.CheckedTextView";
+    public static final String RADIO_BUTTON = "android.widget.RadioButton";
+    public static final String SEEK_BAR = "android.widget.SeekBar";
+    public static final String SPINNER = "android.widget.Spinner";
+    public static final int SHORT_TIMEOUT = 2000;
+    public static final int LONG_TIMEOUT = 5000;
+    public static AccessibilityHelper sInstance = null;
+    private Context mContext = null;
+    private Instrumentation mInstrumentation = null;
+    private UiDevice mDevice = null;
+    private SettingsHelper mSettingsHelper = null;
+
+    public enum SwitchStatus {
+        ON,
+        OFF;
+    }
+
+    private AccessibilityHelper(Instrumentation instr) {
+        mInstrumentation = instr;
+        mSettingsHelper = SettingsHelper.getInstance();
+        mDevice = UiDevice.getInstance(instr);
+        mContext = mInstrumentation.getTargetContext();
+    }
+
+    public static AccessibilityHelper getInstance(Instrumentation instr) {
+        if (sInstance == null) {
+            sInstance = new AccessibilityHelper(instr);
+        }
+        return sInstance;
+    }
+
+    /**
+     * Set Talkback "ON"/"OFF".
+     *
+     * @param value "ON"/"OFF"
+     * @throws Exception
+     */
+    public void setTalkBackSetting(SwitchStatus value) throws Exception {
+        launchSpecificAccessibilitySetting("TalkBack");
+        UiObject2 swtBar = mDevice.wait(
+                Until.findObject(By.res(SETTINGS_PACKAGE, "switch_bar")), SHORT_TIMEOUT)
+                .findObject(By.res(SETTINGS_PACKAGE, "switch_widget"));
+        if (swtBar != null && !swtBar.getText().equals(value.toString())) {
+            swtBar.click();
+            UiObject2 confirmBtn = mDevice.wait(
+                    Until.findObject(By.res("android:id/button1")), LONG_TIMEOUT);
+            if (confirmBtn != null) {
+                confirmBtn.click();
+            }
+            // First time enable talkback, tutorial open.
+            if (mDevice.wait(Until.hasObject(By.text("TalkBack tutorial")), SHORT_TIMEOUT)) {
+                mDevice.pressBack(); // back to talkback setting page
+            }
+        }
+        mDevice.pressBack();
+    }
+
+    /**
+     * Set high contrast "ON"/"OFF".
+     *
+     * @param value "ON"/"OFF"
+     * @throws Exception
+     */
+    public void setHighContrast(SwitchStatus value) throws Exception {
+        launchSpecificAccessibilitySetting("Accessibility");
+        setSettingSwitchValue("High contrast text", value);
+    }
+
+    /**
+     * Launch specific accessibility setting page.
+     *
+     * @param settingName Specific accessibility setting name
+     * @throws Exception
+     */
+    public void launchSpecificAccessibilitySetting(String settingName) throws Exception {
+        mSettingsHelper.launchSettingsPage(mContext, Settings.ACTION_ACCESSIBILITY_SETTINGS);
+        int maxTry = 3;
+        while (maxTry-- >= 0) {
+            Thread.sleep(SHORT_TIMEOUT);
+            UiObject2 actionBar = mDevice.wait(Until.findObject(
+                    By.res(SETTINGS_PACKAGE, "action_bar").enabled(true)), SHORT_TIMEOUT);
+            if (actionBar == null) {
+                mSettingsHelper.launchSettingsPage(mContext,
+                        Settings.ACTION_ACCESSIBILITY_SETTINGS);
+            } else {
+                String actionBarText = actionBar.findObject(By.clazz(TEXT_VIEW)).getText();
+                if (actionBarText.equals(settingName)) {
+                    break;
+                } else if (actionBarText.equals("Accessibility")) {
+                    getSettingFromList(settingName).click();
+                } else {
+                    mDevice.wait(Until.findObject(By.res(SETTINGS_PACKAGE, "action_bar")
+                            .enabled(true)), SHORT_TIMEOUT)
+                            .findObject(By.clazz(IMAGE_BUTTON)).click();
+                }
+            }
+        }
+    }
+
+    /**
+     * Set switch "ON"/"OFF".
+     *
+     * @param settingTag setting name
+     * @param value "ON"/"OFF"
+     * @return true/false
+     * @throws UiObjectNotFoundException
+     * @throws InterruptedException
+     */
+    private boolean setSettingSwitchValue(String settingTag, SwitchStatus value)
+            throws UiObjectNotFoundException, InterruptedException {
+        UiObject2 cellSwitch = getSettingFromList(settingTag)
+                .getParent().getParent().findObject(By.clazz(SWITCH));
+        if (cellSwitch != null) {
+            if (!cellSwitch.getText().equals(value.toString())) {
+                cellSwitch.click();
+                UiObject2 okBtn = mDevice.wait(Until.findObject(
+                        By.res("android:id/button1")), LONG_TIMEOUT);
+                if (okBtn != null) {
+                    okBtn.click();
+                }
+            }
+            return cellSwitch.getText().equals(value.toString());
+        }
+        return false;
+    }
+
+    /**
+     * Get setting name text object from list.
+     *
+     * @param settingName setting name
+     * @return UiObject2
+     * @throws UiObjectNotFoundException
+     */
+    private UiObject2 getSettingFromList(String settingName)
+            throws UiObjectNotFoundException {
+        UiScrollable listScrollable = new UiScrollable(
+                new UiSelector().resourceId(SETTINGS_PACKAGE+":id/list"));
+        if (listScrollable != null) {
+            listScrollable.scrollToBeginning(100);
+            listScrollable.scrollIntoView(
+                    new UiSelector().resourceId("android:id/title").text(settingName));
+            return mDevice.findObject(By.res("android:id/title").text(settingName));
+        } else {
+            throw new UiObjectNotFoundException("Fail to get scrollable list %s.");
+        }
+    }
+}
diff --git a/libraries/system-helpers/accessibility-helper/src/android/system/helpers/AccessibilityScannerHelper.java b/libraries/system-helpers/accessibility-helper/src/android/system/helpers/AccessibilityScannerHelper.java
new file mode 100644
index 0000000..a6da216
--- /dev/null
+++ b/libraries/system-helpers/accessibility-helper/src/android/system/helpers/AccessibilityScannerHelper.java
@@ -0,0 +1,441 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.system.helpers;
+
+import android.app.Instrumentation;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.UiObjectNotFoundException;
+import android.support.test.uiautomator.UiScrollable;
+import android.support.test.uiautomator.UiSelector;
+import android.support.test.uiautomator.Until;
+import android.util.Log;
+
+import java.util.List;
+
+/**
+ * Implement common helper functions for Accessibility scanner.
+ */
+public class AccessibilityScannerHelper {
+    public static final String ACCESSIBILITY_SCANNER_PACKAGE
+            = "com.google.android.apps.accessibility.auditor";
+    public static final String MAIN_ACTIVITY_CLASS = "%s.ui.MainActivity";
+    public static final String CHECK_BUTTON_RES_ID = "accessibilibutton";
+    private static final int SCANNER_WAIT_TIME = 5000;
+    private static final int SHORT_TIMEOUT = 2000;
+    private static final String LOG_TAG = AccessibilityScannerHelper.class.getSimpleName();
+    private static final String RESULT_TAG = "A11Y_SCANNER_RESULT";
+    public static AccessibilityScannerHelper sInstance = null;
+    private UiDevice mDevice = null;
+    private ActivityHelper mActivityHelper = null;
+    private PackageHelper mPackageHelper = null;
+    private AccessibilityHelper mAccessibilityHelper = null;
+
+    private AccessibilityScannerHelper(Instrumentation instr) {
+        mDevice = UiDevice.getInstance(instr);
+        mActivityHelper = ActivityHelper.getInstance();
+        mPackageHelper = PackageHelper.getInstance(instr);
+        mAccessibilityHelper = AccessibilityHelper.getInstance(instr);
+    }
+
+    public static AccessibilityScannerHelper getInstance(Instrumentation instr) {
+        if (sInstance == null) {
+            sInstance = new AccessibilityScannerHelper(instr);
+        }
+        return sInstance;
+    }
+
+    /**
+     * If accessibility scanner installed.
+     *
+     * @return true/false
+     */
+    public boolean scannerInstalled() {
+        return mPackageHelper.isPackageInstalled(ACCESSIBILITY_SCANNER_PACKAGE);
+    }
+
+    /**
+     * Click scanner check button and parse and log results.
+     *
+     * @param resultPrefix
+     * @throws Exception
+     */
+    public void runScanner(String resultPrefix) throws Exception {
+        int tries = 3; // retries
+        while (tries-- > 0) {
+            try {
+                clickScannerCheck();
+                logScannerResult(resultPrefix);
+                break;
+            } catch (UiObjectNotFoundException e) {
+                continue;
+            } catch (Exception e) {
+                throw e;
+            }
+        }
+    }
+
+    /**
+     * Click scanner check button and open share app in the share menu.
+     *
+     * @param resultPrefix
+     * @param shareAppTag
+     * @throws Exception
+     */
+    public void runScannerAndOpenShareApp(String resultPrefix, String shareAppTag)
+            throws Exception {
+        runScanner(resultPrefix);
+        UiObject2 shareApp = getShareApp(shareAppTag);
+        if (shareApp != null) {
+            shareApp.click();
+        }
+    }
+
+    /**
+     * Set Accessibility Scanner setting ON/OFF.
+     *
+     * @throws Exception
+     */
+    public void setAccessibilityScannerSetting(AccessibilityHelper.SwitchStatus value)
+            throws Exception {
+        if (!scannerInstalled()) {
+            throw new Exception("Accessibility Scanner not installed.");
+        }
+        mAccessibilityHelper.launchSpecificAccessibilitySetting("Accessibility Scanner");
+        for (int tries = 0; tries < 2; tries++) {
+            UiObject2 swt = mDevice.wait(Until.findObject(
+                    By.res(AccessibilityHelper.SETTINGS_PACKAGE, "switch_widget")),
+                    SHORT_TIMEOUT * 2);
+            if (swt.getText().equals(value.toString())) {
+                break;
+            } else if (tries == 1) {
+                throw new Exception(String.format("Fail to set scanner to: %s.", value.toString()));
+            } else {
+                swt.click();
+                UiObject2 okBtn = mDevice.wait(Until.findObject(By.text("OK")), SHORT_TIMEOUT);
+                if (okBtn != null) {
+                    okBtn.click();
+                }
+                if (initialSetups()) {
+                    mDevice.pressBack();
+                }
+                grantPermissions();
+            }
+        }
+    }
+
+    /**
+     * Click through all permission pop ups for scanner. Grant all necessary permissions.
+     */
+    private void grantPermissions() {
+        UiObject2 auth1 = mDevice.wait(Until.findObject(
+                By.text("BEGIN AUTHORIZATION")), SHORT_TIMEOUT);
+        if (auth1 != null) {
+            auth1.click();
+        }
+        UiObject2 chk = mDevice.wait(Until.findObject(
+                By.clazz(AccessibilityHelper.CHECK_BOX)), SHORT_TIMEOUT);
+        if (chk != null) {
+            chk.click();
+            mDevice.findObject(By.text("START NOW")).click();
+        }
+        UiObject2 auth2 = mDevice.wait(Until.findObject(
+                By.text("BEGIN AUTHORIZATION")), SHORT_TIMEOUT);
+        if (auth2 != null) {
+            auth2.click();
+        }
+        UiObject2 tapOk = mDevice.wait(Until.findObject(
+                By.pkg(ACCESSIBILITY_SCANNER_PACKAGE).text("OK")), SHORT_TIMEOUT);
+        if (tapOk != null) {
+            tapOk.click();
+        }
+    }
+
+    /**
+     * Launch accessibility scanner.
+     *
+     * @throws UiObjectNotFoundException
+     */
+    public void launchScannerApp() throws Exception {
+        Intent intent = new Intent(Intent.ACTION_MAIN);
+        ComponentName settingComponent = new ComponentName(ACCESSIBILITY_SCANNER_PACKAGE,
+                String.format(MAIN_ACTIVITY_CLASS, ACCESSIBILITY_SCANNER_PACKAGE));
+        intent.setComponent(settingComponent);
+        mActivityHelper.launchIntent(intent);
+        initialSetups();
+    }
+
+    /**
+     * Steps for first time launching scanner app.
+     *
+     * @return true/false return false immediately, if initial setup screen doesn't show up.
+     * @throws Exception
+     */
+    private boolean initialSetups() throws Exception {
+        UiObject2 getStartBtn = mDevice.wait(
+                Until.findObject(By.text("GET STARTED")), SHORT_TIMEOUT);
+        if (getStartBtn != null) {
+            getStartBtn.click();
+            UiObject2 msg = mDevice.wait(Until.findObject(
+                    By.text("Turn on Accessibility Scanner")), SHORT_TIMEOUT);
+            if (msg != null) {
+                mDevice.findObject(By.text("OK")).click();
+                setAccessibilityScannerSetting(AccessibilityHelper.SwitchStatus.ON);
+            }
+            mDevice.wait(Until.findObject(By.text("OK, GOT IT")), SCANNER_WAIT_TIME).click();
+            mDevice.wait(Until.findObject(By.text("DISMISS")), SHORT_TIMEOUT).click();
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Clear history of accessibility scanner.
+     *
+     * @throws InterruptedException
+     */
+    public void clearHistory() throws Exception {
+        launchScannerApp();
+        int maxTry = 20;
+        while (maxTry > 0) {
+            List<UiObject2> historyItemList = mDevice.findObjects(
+                    By.res(ACCESSIBILITY_SCANNER_PACKAGE, "history_item_row"));
+            if (historyItemList.size() == 0) {
+                break;
+            }
+            historyItemList.get(0).click();
+            Thread.sleep(SHORT_TIMEOUT);
+            deleteHistory();
+            Thread.sleep(SHORT_TIMEOUT);
+            maxTry--;
+        }
+    }
+
+    /**
+     * Log results of accessibility scanner.
+     *
+     * @param pageName
+     * @throws Exception
+     */
+    public void logScannerResult(String pageName) throws Exception {
+        int res = getNumberOfSuggestions();
+        if (res > 0) {
+            Log.i(RESULT_TAG, String.format("%s: %s suggestions!", pageName, res));
+        } else if (res == 0) {
+            Log.i(RESULT_TAG, String.format("%s: Pass.", pageName));
+        } else {
+            throw new UiObjectNotFoundException("Fail to get number of suggestions.");
+        }
+    }
+
+    /**
+     * Move scanner button to avoid blocking the object.
+     *
+     * @param avoidObj object to move the check button away from
+     */
+    public void adjustScannerButton(UiObject2 avoidObj)
+            throws UiObjectNotFoundException, InterruptedException {
+        Rect origBounds = getScannerCheckBtn().getVisibleBounds();
+        Rect avoidBounds = avoidObj.getVisibleBounds();
+        if (origBounds.intersect(avoidBounds)) {
+            Point dest = calculateDest(origBounds, avoidBounds);
+            moveScannerCheckButton(dest.x, dest.y);
+        }
+    }
+
+    /**
+     * Move scanner check button back to the middle of the screen.
+     */
+    public void resetScannerCheckButton() throws UiObjectNotFoundException, InterruptedException {
+        int midY = (int) Math.ceil(mDevice.getDisplayHeight() * 0.5);
+        int midX = (int) Math.ceil(mDevice.getDisplayWidth() * 0.5);
+        moveScannerCheckButton(midX, midY);
+    }
+
+    /**
+     * Move scanner check button to a target location.
+     *
+     * @param locX target location x-axis
+     * @param locY target location y-axis
+     * @throws UiObjectNotFoundException
+     */
+    public void moveScannerCheckButton(int locX, int locY)
+            throws UiObjectNotFoundException, InterruptedException {
+        int tries = 2;
+        while (tries-- > 0) {
+            UiObject2 btn = getScannerCheckBtn();
+            Rect bounds = btn.getVisibleBounds();
+            int origX = bounds.centerX();
+            int origY = bounds.centerY();
+            int buttonWidth = bounds.width();
+            int buttonHeight = bounds.height();
+            if (Math.abs(locX - origX) > buttonWidth || Math.abs(locY - origY) > buttonHeight) {
+                btn.drag(new Point(locX, locY));
+            }
+            Thread.sleep(SCANNER_WAIT_TIME);
+            // drag cause a click on the scanner button, bring the UI into scanner app
+            if (getScannerCheckBtn() == null
+                    && mDevice.findObject(By.pkg(ACCESSIBILITY_SCANNER_PACKAGE)) != null) {
+                mDevice.pressBack();
+            } else {
+                break;
+            }
+        }
+    }
+
+    /**
+     * Calculate the moving destination of check button.
+     *
+     * @param origRect original bounds of the check button
+     * @param avoidRect bounds to move away from
+     * @return destination of check button center point.
+     */
+    private Point calculateDest(Rect origRect, Rect avoidRect) {
+        int bufferY = (int)Math.ceil(mDevice.getDisplayHeight() * 0.1);
+        int destY = avoidRect.bottom + bufferY + origRect.height()/2;
+        if (destY >= mDevice.getDisplayHeight()) {
+            destY = avoidRect.top - bufferY - origRect.height()/2;
+        }
+        return new Point(origRect.centerX(), destY);
+    }
+
+    /**
+     * Return scanner check button.
+     *
+     * @return UiObject2
+     */
+    private UiObject2 getScannerCheckBtn() {
+        return mDevice.findObject(By.res(ACCESSIBILITY_SCANNER_PACKAGE, CHECK_BUTTON_RES_ID));
+    }
+
+    private void clickScannerCheck() throws UiObjectNotFoundException, InterruptedException {
+        UiObject2 accessibilityScannerButton = getScannerCheckBtn();
+        if (accessibilityScannerButton != null) {
+            accessibilityScannerButton.click();
+        } else {
+            // TODO: check if app crash error, restart scanner service
+            Log.i(LOG_TAG, "Fail to find accessibility scanner check button.");
+            throw new UiObjectNotFoundException(
+                    "Fail to find accessibility scanner check button.");
+        }
+        Thread.sleep(SCANNER_WAIT_TIME);
+    }
+
+    /**
+     * Check if no suggestion.
+     * @deprecated Use {@link #getNumberOfSuggestions} instead
+     */
+    @Deprecated
+    private Boolean testPass() throws UiObjectNotFoundException {
+        UiObject2 txtView = getToolBarTextView();
+        return txtView.getText().equals("No suggestions");
+    }
+
+    /**
+     * Return accessibility scanner tool bar text view.
+     *
+     * @return UiObject2
+     * @throws UiObjectNotFoundException
+     */
+    private UiObject2 getToolBarTextView() throws UiObjectNotFoundException {
+        UiObject2 toolBar = mDevice.wait(Until.findObject(
+                By.res(ACCESSIBILITY_SCANNER_PACKAGE, "toolbar")), SHORT_TIMEOUT);
+        if (toolBar != null) {
+            return toolBar.findObject(By.clazz(AccessibilityHelper.TEXT_VIEW));
+        } else {
+            throw new UiObjectNotFoundException(
+                    "Failed to find Scanner tool bar. Scanner app might not be active.");
+        }
+    }
+
+    /**
+     * Delete active scanner history.
+     */
+    private void deleteHistory() {
+        UiObject2 moreBtn = mDevice.wait(Until.findObject(By.desc("More options")), SHORT_TIMEOUT);
+        if (moreBtn != null) {
+            moreBtn.click();
+            mDevice.wait(Until.findObject(
+                    By.clazz(AccessibilityHelper.TEXT_VIEW).text("Delete")), SHORT_TIMEOUT).click();
+        }
+    }
+
+    /**
+     * Return number suggestions.
+     *
+     * @return number of suggestions
+     * @throws UiObjectNotFoundException
+     */
+    private int getNumberOfSuggestions() throws UiObjectNotFoundException {
+        int tries = 2; // retries
+        while (tries-- > 0) {
+            UiObject2 txtView = getToolBarTextView();
+            if (txtView != null) {
+                String result = txtView.getText();
+                if (result.equals("No suggestions")) {
+                    return 0;
+                } else {
+                    String str = result.split("\\s+")[0];
+                    return Integer.parseInt(str);
+                }
+            }
+        }
+        Log.i(LOG_TAG, String.format("Error in getting number of suggestions."));
+        return -1;
+    }
+
+    /**
+     * Return share app UiObject2
+     *
+     * @param appName
+     * @return
+     */
+    private UiObject2 getShareApp(String appName) throws UiObjectNotFoundException {
+        UiObject2 shareBtn = mDevice.wait(Until.findObject(By.res(ACCESSIBILITY_SCANNER_PACKAGE,
+                "action_share_results")), SHORT_TIMEOUT);
+        if (shareBtn != null) {
+            shareBtn.click();
+            mDevice.wait(Until.hasObject(By.res("android:id/resolver_list")), SHORT_TIMEOUT * 3);
+            UiScrollable scrollable = new UiScrollable(
+                    new UiSelector().className("android.widget.ScrollView"));
+            int tries = 3;
+            while (!mDevice.hasObject(By.text(appName)) && tries-- > 0) {
+                scrollable.scrollForward();
+            }
+            return mDevice.findObject(By.text(appName));
+        }
+        return null;
+    }
+
+    /**
+     * Return if scanner enabled by check if home screen has check button.
+     *
+     * @return true/false
+     */
+    public boolean ifScannerEnabled() throws InterruptedException {
+        mDevice.pressHome();
+        Thread.sleep(SHORT_TIMEOUT);
+        mDevice.waitForIdle();
+        return getScannerCheckBtn() != null;
+    }
+}
diff --git a/libraries/base-app-helpers/Android.mk b/libraries/system-helpers/account-helper/Android.mk
similarity index 84%
copy from libraries/base-app-helpers/Android.mk
copy to libraries/system-helpers/account-helper/Android.mk
index 0111a0a..e7e20eb 100644
--- a/libraries/base-app-helpers/Android.mk
+++ b/libraries/system-helpers/account-helper/Android.mk
@@ -16,9 +16,11 @@
 LOCAL_PATH := $(call my-dir)
 
 include $(CLEAR_VARS)
-LOCAL_MODULE := base-app-helpers
-LOCAL_STATIC_JAVA_LIBRARIES := dpad-util
-LOCAL_JAVA_LIBRARIES := ub-uiautomator launcher-helper-lib
+LOCAL_MODULE := account-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator \
+    android-support-test \
+    activity-helper \
+    device-helper
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
 include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/system-helpers/account-helper/src/android/system/helpers/AccountHelper.java b/libraries/system-helpers/account-helper/src/android/system/helpers/AccountHelper.java
new file mode 100644
index 0000000..b210bdd
--- /dev/null
+++ b/libraries/system-helpers/account-helper/src/android/system/helpers/AccountHelper.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.system.helpers;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.system.helpers.ActivityHelper;
+import android.system.helpers.DeviceHelper;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import junit.framework.Assert;
+
+/**
+ * Implement common helper methods for account.
+ */
+public class AccountHelper {
+    private static final String TAG = AccountHelper.class.getSimpleName();
+    public static final int TIMEOUT = 1000;
+    private static AccountHelper sInstance = null;
+    private Context mContext = null;
+    private UiDevice mDevice = null;
+    private ActivityHelper mActivityHelper = null;
+    private DeviceHelper mDeviceHelper = null;
+
+    public AccountHelper() {
+        mContext = InstrumentationRegistry.getTargetContext();
+        mActivityHelper = ActivityHelper.getInstance();
+        mDeviceHelper = DeviceHelper.getInstance();
+        mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+    }
+
+    public static AccountHelper getInstance() {
+        if (sInstance == null) {
+            sInstance = new AccountHelper();
+        }
+        return sInstance;
+    }
+
+    /**
+     * Checks whether a google account has been enabled in device for backup
+     * @return true/false
+     * @throws InterruptedException
+     */
+    public boolean hasDeviceBackupAccount() throws InterruptedException {
+        mActivityHelper.launchIntent(android.provider.Settings.ACTION_PRIVACY_SETTINGS);
+        dismissInitalDialogs();
+        Pattern pattern = Pattern.compile("Backup account", Pattern.CASE_INSENSITIVE);
+        if (mDeviceHelper.isNexusExperienceDevice()) {
+          pattern = Pattern.compile("Device backup", Pattern.CASE_INSENSITIVE);
+        }
+        UiObject2 deviceBackup = mDevice.wait(Until.findObject(By.text(pattern)),
+                TIMEOUT * 5);
+        if (deviceBackup!=null){
+            String backupAcct = deviceBackup.getParent().getChildren().get(1).getText();
+            if (backupAcct.equals(getRegisteredGoogleAccountOnDevice())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Get registered accounts ensures there is at least one account registered returns the google
+     * account name
+     * @return The registered gogole/gmail account on device
+     */
+    public String getRegisteredGoogleAccountOnDevice() {
+        Account[] accounts = AccountManager.get(mContext).getAccounts();
+        Assert.assertTrue("Device doesn't have any account registered", accounts.length >= 1);
+        for (int i = 0; i < accounts.length; ++i) {
+            if (accounts[i].type.equals("com.google")) {
+                return accounts[i].name;
+            }
+        }
+        throw new RuntimeException("The device is not registered with a google account");
+    }
+
+    private void dismissInitalDialogs() throws InterruptedException{
+        UiObject2 backupDialog = mDevice.wait(
+                Until.findObject(By.text("Backup & reset")),
+                TIMEOUT);
+        if (backupDialog!=null){
+            backupDialog.click();
+            Thread.sleep(TIMEOUT);
+            UiObject2 alwaysBtn = mDevice.wait(
+                    Until.findObject(By.res("android","button_always")),
+                    TIMEOUT);
+            if (alwaysBtn!=null){
+                alwaysBtn.click();
+            }
+        }
+        Thread.sleep(TIMEOUT);
+    }
+}
diff --git a/libraries/base-app-helpers/Android.mk b/libraries/system-helpers/activity-helper/Android.mk
similarity index 84%
rename from libraries/base-app-helpers/Android.mk
rename to libraries/system-helpers/activity-helper/Android.mk
index 0111a0a..efb71ca 100644
--- a/libraries/base-app-helpers/Android.mk
+++ b/libraries/system-helpers/activity-helper/Android.mk
@@ -16,9 +16,10 @@
 LOCAL_PATH := $(call my-dir)
 
 include $(CLEAR_VARS)
-LOCAL_MODULE := base-app-helpers
-LOCAL_STATIC_JAVA_LIBRARIES := dpad-util
-LOCAL_JAVA_LIBRARIES := ub-uiautomator launcher-helper-lib
+LOCAL_MODULE := activity-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator \
+    android-support-test \
+    commands-helper
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
 include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/system-helpers/activity-helper/src/android/system/helpers/ActivityHelper.java b/libraries/system-helpers/activity-helper/src/android/system/helpers/ActivityHelper.java
new file mode 100644
index 0000000..c768057
--- /dev/null
+++ b/libraries/system-helpers/activity-helper/src/android/system/helpers/ActivityHelper.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.system.helpers;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.RemoteException;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiScrollable;
+import android.support.test.uiautomator.UiSelector;
+import android.support.test.uiautomator.Until;
+import android.util.Log;
+import android.view.KeyEvent;
+
+import junit.framework.Assert;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Implement common helper methods for activities.
+ */
+public class ActivityHelper {
+    private static final String TAG = ActivityHelper.class.getSimpleName();
+
+    public static final String SYSTEMUI_PACKAGE = "com.android.systemui";
+    public static final int FULLSCREEN = 1;
+    public static final int SPLITSCREEN = 3;
+    public static final int TIMEOUT = 1000;
+    public static final int INVALID_TASK_ID = -1;
+
+    private static ActivityHelper sInstance = null;
+    private Context mContext = null;
+    private UiDevice mDevice = null;
+
+    public ActivityHelper() {
+        mContext = InstrumentationRegistry.getTargetContext();
+        mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+    }
+
+    public static ActivityHelper getInstance() {
+        if (sInstance == null) {
+            sInstance = new ActivityHelper();
+        }
+        return sInstance;
+    }
+
+    /**
+     * Gets task id for an activity
+     *
+     * @param pkgName
+     * @param activityName
+     * @return taskId or -1 when no activity is found
+     */
+    public int getTaskIdForActivity(String pkgName, String activityName) {
+        int taskId = INVALID_TASK_ID;
+        // Find task id for given package and activity
+        final Pattern TASK_REGEX = Pattern.compile(
+                String.format("taskId=(\\d+): %s/%s", pkgName, activityName));
+        Matcher matcher = TASK_REGEX.matcher(CommandsHelper.execute("am stack list"));
+        if (matcher.find()) {
+            taskId = Integer.parseInt(matcher.group(1));
+            Log.i(TAG, String.format("TaskId found: %d for %s/%s",
+                    taskId, pkgName, activityName));
+        }
+        Assert.assertTrue("Taskid hasn't been found", taskId != -1);
+        return taskId;
+    }
+
+    /**
+     * Helper to change window mode between fullscreen and splitscreen for a given task
+     *
+     * @param taskId
+     * @param mode
+     * @throws InterruptedException
+     */
+    public void changeWindowMode(int taskId, int mode) throws InterruptedException {
+        CommandsHelper.execute(
+                String.format("am stack move-task %d %d true", taskId, mode));
+        Thread.sleep(TIMEOUT);
+    }
+
+    /**
+     * Clears apps in overview/recents
+     *
+     * @throws InterruptedException
+     * @throws RemoteException
+     */
+    public void clearRecents() throws InterruptedException, RemoteException {
+        // Launch recents if it's not already
+        int retry = 5;
+        while (!mDevice.wait(Until.hasObject(By.res(SYSTEMUI_PACKAGE, "recents_view")),
+                TIMEOUT * 5) && --retry > 0) {
+            mDevice.pressRecentApps();
+            Thread.sleep(TIMEOUT);
+        }
+        // Return if there is no apps in recents
+        if (mDevice.wait(Until.hasObject(By.text("No recent items")), TIMEOUT * 5)) {
+            return;
+        } else {
+            Assert.assertTrue("Device expects recent items", mDevice.wait(Until.hasObject(
+                    By.res(SYSTEMUI_PACKAGE, "recents_view")), TIMEOUT * 5));
+        }
+        // Get recents items
+        int recents = mDevice.wait(Until.findObjects(
+                By.res(SYSTEMUI_PACKAGE, "task_view_thumbnail")), TIMEOUT * 5).size();
+        // Clear recents
+        for (int i = 0; i < recents; ++i) {
+            mDevice.pressKeyCode(KeyEvent.KEYCODE_APP_SWITCH);
+            Thread.sleep(TIMEOUT);
+            mDevice.pressKeyCode(KeyEvent.KEYCODE_DEL);
+            Thread.sleep(TIMEOUT);
+        }
+    }
+
+    /**
+     * Clear recent apps by click 'CLEAR ALL' button in the recents view.
+     *
+     * @throws Exception
+     */
+    public void clearRecentsByClearAll() throws Exception {
+        int retry = 5;
+        while (!mDevice.wait(Until.hasObject(By.res(SYSTEMUI_PACKAGE, "recents_view")),
+                TIMEOUT * 5) && --retry > 0) {
+            mDevice.pressRecentApps();
+            Thread.sleep(TIMEOUT);
+        }
+        int maxTries = 20;
+        while (!mDevice.hasObject(By.text("No recent items")) && maxTries-- >= 0) {
+            UiScrollable thumbnailScrollable = new UiScrollable(
+                    new UiSelector().className("android.widget.ScrollView"));
+            thumbnailScrollable.scrollToBeginning(100);
+            if (!mDevice.wait(Until.hasObject(By.text("CLEAR ALL")), TIMEOUT * 2)) {
+                continue;
+            } else {
+                int tries = 3;
+                while (mDevice.hasObject(By.text("CLEAR ALL")) && tries-- > 0) {
+                    mDevice.findObject(By.text("CLEAR ALL")).click();
+                    Thread.sleep(TIMEOUT * 2);
+                }
+                break;
+            }
+        }
+    }
+
+    /**
+     * Enable/disable bmgr service
+     *
+     * @param enable true to enable, false to disable
+     */
+    public void enableBmgr(boolean enable) {
+        String output = CommandsHelper.execute("bmgr enable " + Boolean.toString(enable));
+        if (enable) {
+            Assert.assertTrue("Bmgr not enabled",
+                    output.indexOf("Backup Manager now enabled") >= 0);
+        } else {
+            Assert.assertTrue("Bmgr not disabled",
+                    output.indexOf("Backup Manager now disabled") >= 0);
+        }
+    }
+
+    /**
+     * Launch an intent when intent is of string type
+     *
+     * @param intentName
+     * @throws InterruptedException
+     */
+    public void launchIntent(String intentName) throws InterruptedException {
+        mDevice.pressHome();
+        Intent intent = new Intent(intentName);
+        launchIntent(intent);
+    }
+
+    /**
+     * Find intent of a package and launch
+     *
+     * @param pkgName
+     * @throws InterruptedException
+     */
+    public void launchPackage(String pkgName) throws InterruptedException {
+        Intent pkgIntent = mContext.getPackageManager()
+                .getLaunchIntentForPackage(pkgName);
+        launchIntent(pkgIntent);
+    }
+
+    /**
+     * launch an intent when intent is of Intent type
+     *
+     * @param intent
+     * @throws InterruptedException
+     */
+    public void launchIntent(Intent intent) throws InterruptedException {
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        mContext.startActivity(intent);
+        Thread.sleep(TIMEOUT * 5);
+    }
+}
diff --git a/libraries/base-app-helpers/Android.mk b/libraries/system-helpers/commands-helper/Android.mk
similarity index 84%
copy from libraries/base-app-helpers/Android.mk
copy to libraries/system-helpers/commands-helper/Android.mk
index 0111a0a..15f1c66 100644
--- a/libraries/base-app-helpers/Android.mk
+++ b/libraries/system-helpers/commands-helper/Android.mk
@@ -16,9 +16,9 @@
 LOCAL_PATH := $(call my-dir)
 
 include $(CLEAR_VARS)
-LOCAL_MODULE := base-app-helpers
-LOCAL_STATIC_JAVA_LIBRARIES := dpad-util
-LOCAL_JAVA_LIBRARIES := ub-uiautomator launcher-helper-lib
+LOCAL_MODULE := commands-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator \
+    android-support-test
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
 include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/system-helpers/commands-helper/src/android/system/helpers/CommandsHelper.java b/libraries/system-helpers/commands-helper/src/android/system/helpers/CommandsHelper.java
new file mode 100644
index 0000000..0f8783d
--- /dev/null
+++ b/libraries/system-helpers/commands-helper/src/android/system/helpers/CommandsHelper.java
@@ -0,0 +1,102 @@
+package android.system.helpers;
+
+import android.app.Instrumentation;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.uiautomator.UiDevice;
+import android.util.Log;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Implement common helper for executing shell commands on device
+ */
+public class CommandsHelper {
+    private static final String TAG = CommandsHelper.class.getSimpleName();
+    private static CommandsHelper sInstance = null;
+    private Instrumentation mInstrumentation = null;
+
+    private static final String LINE_SEPARATORS = "\\r?\\n";
+
+
+    private CommandsHelper(Instrumentation instrumentation) {
+        mInstrumentation = instrumentation;
+    }
+
+    /**
+     * @deprecated Should use {@link CommandsHelper#getInstance(Instrumentation)} instead.
+     */
+    @Deprecated
+    public static CommandsHelper getInstance() {
+        if (sInstance == null) {
+            sInstance = new CommandsHelper(InstrumentationRegistry.getInstrumentation());
+        }
+        return sInstance;
+    }
+
+    public static CommandsHelper getInstance(Instrumentation instrumentation) {
+        if (sInstance == null) {
+            sInstance = new CommandsHelper(instrumentation);
+        } else {
+            sInstance.injectInstrumentation(instrumentation);
+        }
+        return sInstance;
+    }
+
+    /**
+     * Injects instrumentation into this helper.
+     *
+     * @param instrumentation the instrumentation to use with this instance
+     */
+    public void injectInstrumentation(Instrumentation instrumentation) {
+        mInstrumentation = instrumentation;
+    }
+
+    /**
+     * Executes a shell command on device, and return the standard output in string.
+     * @param command the command to run
+     * @return the standard output of the command, or empty string if
+     * failed without throwing an IOException
+     */
+    public String executeShellCommand(String command) {
+        try {
+            return UiDevice.getInstance(mInstrumentation).executeShellCommand(command);
+        } catch (IOException e) {
+            // ignore
+            Log.e(TAG, String.format("The shell command failed to run: %s exception: %s",
+                    command, e.getMessage()));
+            return "";
+        }
+    }
+
+    /**
+     * Executes a shell command on device, and split the multi-line output into collection
+     * @param command the command to run
+     * @param separatorChars the line separator
+     * @return the List of strings from the standard output of the command
+     */
+    public List<String> executeShellCommandAndSplitOutput(String command,
+            final String separatorChars) {
+        return Arrays.asList(executeShellCommand(command).split(separatorChars));
+    }
+
+    /**
+     * Convenience version of {@link #executeShellCommand} for use without having a reference to
+     * CommandsHelper.
+     * @param command the command to run
+     */
+    @Deprecated
+    public static String execute(String command) {
+        return getInstance().executeShellCommand(command);
+    }
+
+    /**
+     * Convenience version of {@link #executeShellCommandAndSplitOutput} for use
+     * without having a reference to CommandsHelper.
+     * @param command the command to run
+     */
+    @Deprecated
+    public static List<String> executeAndSplitLines(String command) {
+        return getInstance().executeShellCommandAndSplitOutput(command, LINE_SEPARATORS);
+    }
+}
diff --git a/libraries/base-app-helpers/Android.mk b/libraries/system-helpers/connectivity-helper/Android.mk
similarity index 84%
copy from libraries/base-app-helpers/Android.mk
copy to libraries/system-helpers/connectivity-helper/Android.mk
index 0111a0a..4ed55aa 100644
--- a/libraries/base-app-helpers/Android.mk
+++ b/libraries/system-helpers/connectivity-helper/Android.mk
@@ -16,9 +16,10 @@
 LOCAL_PATH := $(call my-dir)
 
 include $(CLEAR_VARS)
-LOCAL_MODULE := base-app-helpers
-LOCAL_STATIC_JAVA_LIBRARIES := dpad-util
-LOCAL_JAVA_LIBRARIES := ub-uiautomator launcher-helper-lib
+LOCAL_MODULE := connectivity-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator \
+    android-support-test \
+    commands-helper
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
 include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/system-helpers/connectivity-helper/src/android/system/helpers/ConnectivityHelper.java b/libraries/system-helpers/connectivity-helper/src/android/system/helpers/ConnectivityHelper.java
new file mode 100644
index 0000000..9006afc
--- /dev/null
+++ b/libraries/system-helpers/connectivity-helper/src/android/system/helpers/ConnectivityHelper.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.system.helpers;
+
+import android.app.DownloadManager;
+import android.bluetooth.BluetoothAdapter;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkInfo;
+import android.net.wifi.WifiInfo;
+import android.net.wifi.WifiManager;
+import android.support.test.InstrumentationRegistry;
+import android.system.helpers.CommandsHelper;
+import android.telecom.TelecomManager;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import junit.framework.Assert;
+
+/**
+ * Implement common helper methods for connectivity.
+ */
+public class ConnectivityHelper {
+    private static final String TAG = ConnectivityHelper.class.getSimpleName();
+    private final static String DEFAULT_PING_SITE = "http://www.google.com";
+
+    public static final int TIMEOUT = 1000;
+    private static ConnectivityHelper sInstance = null;
+    private Context mContext = null;
+    private CommandsHelper mCommandsHelper = null;
+
+    public ConnectivityHelper() {
+        mContext = InstrumentationRegistry.getTargetContext();
+        mCommandsHelper = CommandsHelper.getInstance();
+    }
+
+    public static ConnectivityHelper getInstance() {
+        if (sInstance == null) {
+            sInstance = new ConnectivityHelper();
+        }
+        return sInstance;
+    }
+
+    public TelecomManager getTelecomManager() {
+        return (TelecomManager) mContext.getSystemService(Context.TELECOM_SERVICE);
+    }
+
+    public WifiManager getWifiManager() {
+        return (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
+    }
+
+    public ConnectivityManager getConnectivityManager() {
+        return (ConnectivityManager) (ConnectivityManager) mContext
+                .getSystemService(Context.CONNECTIVITY_SERVICE);
+    }
+
+    public DownloadManager getDownloadManager() {
+        return (DownloadManager) (DownloadManager) mContext
+                .getSystemService(Context.DOWNLOAD_SERVICE);
+    }
+
+    public BluetoothAdapter getBluetoothAdapter() {
+        return BluetoothAdapter.getDefaultAdapter();
+    }
+
+   /**
+     * Checks if device connection is active either through wifi or mobile data by sending an HTTP
+     * request, check for HTTP_OK
+     */
+    public boolean isConnected() throws InterruptedException {
+        int counter = 7;
+        long TIMEOUT_MS = TIMEOUT;
+        HttpURLConnection conn = null;
+        while (--counter > 0) {
+            try{
+                URL url = new URL(DEFAULT_PING_SITE);
+                conn = (HttpURLConnection) url.openConnection();
+                conn.setRequestMethod("GET");
+                conn.setConnectTimeout(TIMEOUT * 60); // 1 minute
+                conn.setReadTimeout(TIMEOUT * 60); // 1 minute
+                Log.i(TAG, "connection response code is " + conn.getResponseCode());
+                Log.i(TAG, " counter = " + counter);
+                return true;
+            } catch (IOException ex) {
+                // Wifi being flaky in the lab, test retries 10 times to connect to google.com
+                // as IOException is throws connection isn't made and response stream is null
+                // so for retrying purpose, exception hasn't been rethrown
+                Log.i(TAG, ex.getMessage());
+            } finally {
+                if (conn != null) {
+                    conn.disconnect();
+                }
+            }
+            Thread.sleep(TIMEOUT_MS);
+            TIMEOUT_MS = 2 * TIMEOUT_MS;
+        }
+        Log.i(TAG, " counter = " + counter);
+        return false;
+    }
+
+    /**
+     * Disconnects and disables network
+     * @return true/false
+     */
+    public int disconnectWifi() {
+        Assert.assertTrue("Wifi not disconnected", getWifiManager().disconnect());
+        int netId = getWifiManager().getConnectionInfo().getNetworkId();
+        getWifiManager().disableNetwork(netId);
+        getWifiManager().saveConfiguration();
+        return netId;
+    }
+
+    /**
+     * Ensures wifi is enabled in device
+     * @throws InterruptedException
+     */
+    public void ensureWifiEnabled() throws InterruptedException {
+        // Device already connected to wifi as part of tradefed setup
+        if (!getWifiManager().isWifiEnabled()) {
+            getWifiManager().enableNetwork(getWifiManager().getConnectionInfo().getNetworkId(),
+                    true);
+            int counter = 5;
+            while (--counter > 0 && !getWifiManager().isWifiEnabled()) {
+                Thread.sleep(TIMEOUT * 5);
+            }
+        }
+        Assert.assertTrue("Wifi should be enabled by now", getWifiManager().isWifiEnabled());
+    }
+
+    /**
+     * Checks whether device has wifi connection for data service
+     * @return true/false
+     */
+    public boolean hasWifiData() {
+        NetworkInfo netInfo = getConnectivityManager().getActiveNetworkInfo();
+        Assert.assertNotNull(netInfo);
+        return (netInfo.getType() == ConnectivityManager.TYPE_WIFI);
+    }
+
+    /**
+     * Checks whether device has mobile connection for data service
+     * @return true/false
+     */
+    public boolean hasMobileData() {
+        NetworkInfo netInfo = getConnectivityManager().getActiveNetworkInfo();
+        Assert.assertNotNull(netInfo);
+        return (netInfo.getType() == ConnectivityManager.TYPE_MOBILE);
+    }
+
+    /**
+     * Checks whether device has sim
+     * @return true/false
+     */
+    public boolean hasDeviceSim() {
+        TelephonyManager telMgr = (TelephonyManager) mContext
+                .getSystemService(mContext.TELEPHONY_SERVICE);
+        return (telMgr.getSimState() == TelephonyManager.SIM_STATE_READY);
+    }
+
+    /**
+     * Get connected wifi SSID.
+     * @return connected wifi SSID
+     */
+    public String getCurrentWifiSSID() {
+        WifiInfo connectionInfo = getWifiManager().getConnectionInfo();
+        if (connectionInfo != null) {
+            return connectionInfo.getSSID();
+        }
+        return null;
+    }
+
+    /**
+     * Ensure bluetooth is enabled on device.
+     * @throws InterruptedException
+     */
+    public void ensureBluetoothEnabled() throws InterruptedException {
+        if (!getBluetoothAdapter().isEnabled()) {
+            getBluetoothAdapter().enable();
+        }
+        int counter = 5;
+        while (--counter > 0 && !getBluetoothAdapter().isEnabled()) {
+            Thread.sleep(TIMEOUT * 2);
+        }
+    }
+
+    /**
+     * Check whether device has mobile data available.
+     * @return true/false
+     */
+    public boolean mobileDataAvailable() {
+        Network[] networkArray = getConnectivityManager().getAllNetworks();
+        for (Network net: networkArray) {
+            if (getConnectivityManager().getNetworkInfo(net).getType()
+                    == ConnectivityManager.TYPE_MOBILE) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
diff --git a/libraries/base-app-helpers/Android.mk b/libraries/system-helpers/device-helper/Android.mk
similarity index 84%
copy from libraries/base-app-helpers/Android.mk
copy to libraries/system-helpers/device-helper/Android.mk
index 0111a0a..cd32d11 100644
--- a/libraries/base-app-helpers/Android.mk
+++ b/libraries/system-helpers/device-helper/Android.mk
@@ -16,9 +16,8 @@
 LOCAL_PATH := $(call my-dir)
 
 include $(CLEAR_VARS)
-LOCAL_MODULE := base-app-helpers
-LOCAL_STATIC_JAVA_LIBRARIES := dpad-util
-LOCAL_JAVA_LIBRARIES := ub-uiautomator launcher-helper-lib
+LOCAL_MODULE := device-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator android-support-test
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
 include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/system-helpers/device-helper/src/android/system/helpers/DeviceHelper.java b/libraries/system-helpers/device-helper/src/android/system/helpers/DeviceHelper.java
new file mode 100644
index 0000000..73ea852
--- /dev/null
+++ b/libraries/system-helpers/device-helper/src/android/system/helpers/DeviceHelper.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.system.helpers;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.RemoteException;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.uiautomator.UiDevice;
+import android.view.WindowManager;
+import android.util.DisplayMetrics;
+
+/**
+ * Implement common helper methods for device.
+ */
+public class DeviceHelper {
+
+    public static final String PIXEL_XL = "Pixel XL";
+    public static final String PIXEL = "Pixel";
+    public static final String RYU = "Pixel C";
+    // 600dp is the threshold value for 7-inch tablets.
+    private static final int TABLET_DP_THRESHOLD = 600;
+    public static final int LONG_TIMEOUT = 2000;
+    private static DeviceHelper sInstance = null;
+    private Context mContext = null;
+    private UiDevice mDevice = null;
+
+    public DeviceHelper() {
+        mContext = InstrumentationRegistry.getTargetContext();
+        mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+    }
+
+    public static DeviceHelper getInstance() {
+        if (sInstance == null) {
+            sInstance = new DeviceHelper();
+        }
+        return sInstance;
+    }
+
+    /** Returns true if the device is a tablet */
+    public boolean isTablet() {
+        // Get screen density & screen size from window manager
+        WindowManager wm = (WindowManager) mContext.getSystemService(
+                Context.WINDOW_SERVICE);
+        DisplayMetrics metrics = new DisplayMetrics();
+        wm.getDefaultDisplay().getMetrics(metrics);
+        // Determines the smallest screen width DP which is
+        // calculated as ( pixels * density - independent pixel unit ) / density.
+        // http://developer.android.com/guide/practices/screens_support.html.
+        int screenDensity = metrics.densityDpi;
+        int screenWidth = Math.min(
+                metrics.widthPixels, metrics.heightPixels);
+        int screenHeight = Math.max(
+                metrics.widthPixels, metrics.heightPixels);
+        int smallestScreenWidthDp = (Math.min(screenWidth, screenHeight)
+                * DisplayMetrics.DENSITY_DEFAULT) / screenDensity;
+        return smallestScreenWidthDp >= TABLET_DP_THRESHOLD;
+    }
+
+    public boolean isNexusExperienceDevice() {
+        // Get device model
+        String result = Build.MODEL;
+        if (result.trim().equalsIgnoreCase(PIXEL) || result.trim().equalsIgnoreCase(PIXEL_XL)) {
+            return true;
+        }
+        return false;
+    }
+
+    public boolean isRyuDevice() {
+        return Build.MODEL.trim().equalsIgnoreCase(RYU);
+    }
+
+    /**
+     * Device sleep and wake up
+     * @throws RemoteException, InterruptedException
+     */
+    public void sleepAndWakeUpDevice() throws RemoteException, InterruptedException {
+        mDevice.sleep();
+        Thread.sleep(LONG_TIMEOUT);
+        mDevice.wakeUp();
+    }
+}
diff --git a/libraries/base-app-helpers/Android.mk b/libraries/system-helpers/package-helper/Android.mk
similarity index 84%
copy from libraries/base-app-helpers/Android.mk
copy to libraries/system-helpers/package-helper/Android.mk
index 0111a0a..3a06fc6 100644
--- a/libraries/base-app-helpers/Android.mk
+++ b/libraries/system-helpers/package-helper/Android.mk
@@ -16,9 +16,9 @@
 LOCAL_PATH := $(call my-dir)
 
 include $(CLEAR_VARS)
-LOCAL_MODULE := base-app-helpers
-LOCAL_STATIC_JAVA_LIBRARIES := dpad-util
-LOCAL_JAVA_LIBRARIES := ub-uiautomator launcher-helper-lib
+LOCAL_MODULE := package-helper
+LOCAL_JAVA_LIBRARIES := commands-helper
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_SDK_VERSION := current
 
 include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/system-helpers/package-helper/src/android/system/helpers/PackageHelper.java b/libraries/system-helpers/package-helper/src/android/system/helpers/PackageHelper.java
new file mode 100644
index 0000000..5faff90
--- /dev/null
+++ b/libraries/system-helpers/package-helper/src/android/system/helpers/PackageHelper.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.system.helpers;
+
+import android.app.Instrumentation;
+import android.content.pm.PackageManager;
+import android.os.SystemClock;
+import android.system.helpers.CommandsHelper;
+
+/**
+ * Implement common helper methods for package management.
+ * eg. Delete package data.
+ */
+public class PackageHelper {
+    public final int TIMEOUT = 500;
+    public static PackageHelper sInstance = null;
+    private Instrumentation mInstrumentation = null;
+    private CommandsHelper cmdHelper = null;
+    private PackageManager mPackageManager = null;
+
+    public PackageHelper(Instrumentation instrumentation) {
+        mInstrumentation = instrumentation;
+        cmdHelper = CommandsHelper.getInstance(instrumentation);
+        mPackageManager = instrumentation.getTargetContext().getPackageManager();
+    }
+
+    public static PackageHelper getInstance(Instrumentation instrumentation) {
+        if (sInstance == null) {
+            sInstance = new PackageHelper(instrumentation);
+        }
+        return sInstance;
+    }
+
+    /**
+     * Deletes all data associated with the package.
+     * @param packageName package name
+     */
+    public void cleanPackage(String packageName) {
+        cmdHelper.executeShellCommand(String.format("pm clear %s", packageName));
+        SystemClock.sleep(2 * TIMEOUT);
+    }
+
+    /**
+     * Check if certain package is installed on the device.
+     * @param packageName package name
+     * @return true/false
+     */
+    public Boolean isPackageInstalled(String packageName) {
+        try {
+            mPackageManager.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES);
+            return true;
+        } catch (PackageManager.NameNotFoundException e) {
+            return false;
+        }
+    }
+}
diff --git a/libraries/base-app-helpers/Android.mk b/libraries/system-helpers/permission-helper/Android.mk
similarity index 82%
copy from libraries/base-app-helpers/Android.mk
copy to libraries/system-helpers/permission-helper/Android.mk
index 0111a0a..5ac70a6 100644
--- a/libraries/base-app-helpers/Android.mk
+++ b/libraries/system-helpers/permission-helper/Android.mk
@@ -16,9 +16,11 @@
 LOCAL_PATH := $(call my-dir)
 
 include $(CLEAR_VARS)
-LOCAL_MODULE := base-app-helpers
-LOCAL_STATIC_JAVA_LIBRARIES := dpad-util
-LOCAL_JAVA_LIBRARIES := ub-uiautomator launcher-helper-lib
+LOCAL_MODULE := permission-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator \
+    launcher-helper-lib \
+    android-support-test
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_SDK_VERSION := current
 
 include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/tests/functional/permission/src/com/android/functional/permissiontests/PermissionHelper.java b/libraries/system-helpers/permission-helper/src/android/system/helpers/PermissionHelper.java
similarity index 89%
rename from tests/functional/permission/src/com/android/functional/permissiontests/PermissionHelper.java
rename to libraries/system-helpers/permission-helper/src/android/system/helpers/PermissionHelper.java
index d6edd8a..092dbd8 100644
--- a/tests/functional/permission/src/com/android/functional/permissiontests/PermissionHelper.java
+++ b/libraries/system-helpers/permission-helper/src/android/system/helpers/PermissionHelper.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.functional.permissiontests;
+package android.system.helpers;
 
 import android.app.UiAutomation;
 import android.content.Context;
@@ -25,6 +25,7 @@
 import android.os.SystemClock;
 import android.support.test.launcherhelper.ILauncherStrategy;
 import android.support.test.launcherhelper.LauncherStrategyFactory;
+import android.support.test.InstrumentationRegistry;
 import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.Direction;
 import android.support.test.uiautomator.UiDevice;
@@ -36,42 +37,59 @@
 
 import java.io.BufferedReader;
 import java.io.FileInputStream;
-import java.io.IOException;
 import java.io.InputStreamReader;
-import java.util.ArrayList;
+import java.io.IOException;
 import java.util.Arrays;
-import java.util.Hashtable;
+import java.util.ArrayList;
 import java.util.List;
+import java.util.Hashtable;
 
+/**
+ * Implement common helper methods for permissions.
+ */
 public class PermissionHelper {
     public static final String TEST_TAG = "PermissionTest";
     public static final String SETTINGS_PACKAGE = "com.android.settings";
+    public static final int REQUESTED_PERMISSION_FLAG_GRANTED = 3;
+    public static final int REQUESTED_PERMISSION_FLAG_DENIED = 1;
     public final int TIMEOUT = 500;
     public static PermissionHelper mInstance = null;
-    private UiDevice mDevice;
-    private Context mContext;
-    private static UiAutomation mUiAutomation;
+    private UiDevice mDevice = null;
+    private Context mContext = null;
+    private static UiAutomation mUiAutomation = null;
     public static Hashtable<String, List<String>> mPermissionGroupInfo = null;
-    ILauncherStrategy mLauncherStrategy;
+    ILauncherStrategy mLauncherStrategy = null;
 
-    private PermissionHelper(UiDevice device, Context context, UiAutomation uiAutomation) {
-        mDevice = device;
-        mContext = context;
-        mUiAutomation = uiAutomation;
+    /** Supported operations on permission */
+    public enum PermissionOp {
+        GRANT, REVOKE;
+    }
+
+    /** Available permission status */
+    public enum PermissionStatus {
+        ON, OFF;
+    }
+
+    private PermissionHelper() {
+        mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        mContext = InstrumentationRegistry.getTargetContext();
+        mUiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
         mLauncherStrategy = LauncherStrategyFactory.getInstance(mDevice).getLauncherStrategy();
     }
 
-    public static PermissionHelper getInstance(UiDevice device, Context context,
-            UiAutomation uiAutomation) {
+    public static PermissionHelper getInstance() {
         if (mInstance == null) {
-            mInstance = new PermissionHelper(device, context, uiAutomation);
+            mInstance = new PermissionHelper();
             PermissionHelper.populateDangerousPermissionGroupInfo();
         }
         return mInstance;
     }
 
     /**
-     * Returns list of all dangerous permission of the system
+     * Populates a list of all dangerous permission of the system
+     * Dangerous permissions are higher-risk permissoins that grant requesting applications
+     * access to private user data or control over the device that can negatively impact
+     * the user
      */
     private static void populateDangerousPermissionGroupInfo() {
         ParcelFileDescriptor pfd = mUiAutomation.executeShellCommand("pm list permissions -g -d");
@@ -81,9 +99,7 @@
             List<String> permissions = new ArrayList<String>();
             String groupName = null;
             while ((line = reader.readLine()) != null) {
-                if (line.isEmpty()) {
-                    // Do nothing
-                } else if (line.startsWith("group")) {
+                if (line.startsWith("group")) {
                     if (mPermissionGroupInfo == null) {
                         mPermissionGroupInfo = new Hashtable<String, List<String>>();
                     } else {
@@ -102,10 +118,10 @@
     }
 
     /**
-     * Returns list of permission asked by package
+     * Returns list of granted/denied permission asked by package
      * @param packageName : PackageName for which permission list to be returned
      * @param permitted : set 'true' for normal and default granted dangerous permissions, 'false'
-     * otherwise
+     * for permissions currently denied by package
      * @return
      */
     public List<String> getPermissionByPackage(String packageName, Boolean permitted) {
@@ -114,7 +130,7 @@
         int[] requestedPermissionFlags = null;
         PackageInfo packageInfo = null;
         try {
-            packageInfo = getPackageManager().getPackageInfo(packageName,
+            packageInfo = mContext.getPackageManager().getPackageInfo(packageName,
                     PackageManager.GET_PERMISSIONS);
         } catch (NameNotFoundException e) {
             throw new RuntimeException(String.format("%s package isn't found", packageName));
@@ -124,9 +140,11 @@
         requestedPermissionFlags = packageInfo.requestedPermissionsFlags;
         for (int i = 0; i < requestedPermissions.length; ++i) {
             // requestedPermissionFlags 1 = Denied, 3 = Granted
-            if (permitted && requestedPermissionFlags[i] == 3) {
+            if (permitted && requestedPermissionFlags[i]
+                == REQUESTED_PERMISSION_FLAG_GRANTED) {
                 selectedPermissions.add(requestedPermissions[i]);
-            } else if (!permitted && requestedPermissionFlags[i] == 1) {
+            } else if (!permitted && requestedPermissionFlags[i]
+                == REQUESTED_PERMISSION_FLAG_DENIED) {
                 selectedPermissions.add(requestedPermissions[i]);
             }
         }
@@ -253,13 +271,15 @@
      */
     public void openAppPermissionView(String appName) {
         mDevice.pressHome();
-        launchApp(SETTINGS_PACKAGE, "Settings");
+        if (!mDevice.hasObject(By.pkg(SETTINGS_PACKAGE).depth(0))) {
+            mLauncherStrategy.launch("Settings", SETTINGS_PACKAGE);
+        }
         UiObject2 app = null;
         UiObject2 view = null;
         int maxAttempt = 5;
         while ((maxAttempt-- > 0)
                 && ((app = mDevice.wait(Until.findObject(By.res("android:id/title").text("Apps")),
-                        TIMEOUT)) == null)) {
+                TIMEOUT)) == null)) {
             view = mDevice.wait(Until.findObject(By.res(SETTINGS_PACKAGE, "main_content")),
                     TIMEOUT);
             // todo scroll may be different for device and build
@@ -274,7 +294,7 @@
         maxAttempt = 10;
         while ((maxAttempt-- > 0)
                 && ((app = mDevice.wait(Until.findObject(By.res("android:id/title").text(appName)),
-                        TIMEOUT)) == null)) {
+                TIMEOUT)) == null)) {
             view = mDevice.wait(Until.findObject(By.res("com.android.settings:id/main_content")),
                     TIMEOUT);
             // todo scroll may be different for device and build
@@ -357,8 +377,8 @@
     }
 
     /**
-     * To ensure that all default dangerous permissions mentioned in manifest are granted for any
-     * privileged app
+     * Set package permissions to ensure that all default dangerous permissions
+     * mentioned in manifest are granted for any privileged app
      * @param packageName
      * @param granted
      * @param denied
@@ -402,29 +422,4 @@
         }
         return groupNames;
     }
-
-    public PackageManager getPackageManager() {
-        return mContext.getPackageManager();
-    }
-
-    public void launchApp(String packageName, String appName) {
-        if (!mDevice.hasObject(By.pkg(packageName).depth(0))) {
-            mLauncherStrategy.launch(appName, packageName);
-        }
-    }
-
-    public void cleanPackage(String packageName) {
-            mUiAutomation.executeShellCommand(String.format("pm clear %s", packageName));
-            SystemClock.sleep(2 * TIMEOUT);
-    }
-
-    /** Supported operations on permission */
-    public enum PermissionOp {
-        GRANT, REVOKE;
-    }
-
-    /** Available permission status */
-    public enum PermissionStatus {
-        ON, OFF;
-    }
 }
diff --git a/libraries/base-app-helpers/Android.mk b/libraries/system-helpers/settings-helper/Android.mk
similarity index 84%
copy from libraries/base-app-helpers/Android.mk
copy to libraries/system-helpers/settings-helper/Android.mk
index 0111a0a..74e0098 100644
--- a/libraries/base-app-helpers/Android.mk
+++ b/libraries/system-helpers/settings-helper/Android.mk
@@ -16,9 +16,10 @@
 LOCAL_PATH := $(call my-dir)
 
 include $(CLEAR_VARS)
-LOCAL_MODULE := base-app-helpers
-LOCAL_STATIC_JAVA_LIBRARIES := dpad-util
-LOCAL_JAVA_LIBRARIES := ub-uiautomator launcher-helper-lib
+LOCAL_MODULE := settings-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator \
+    android-support-test \
+    activity-helper
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
 include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/system-helpers/settings-helper/src/android/system/helpers/SettingsHelper.java b/libraries/system-helpers/settings-helper/src/android/system/helpers/SettingsHelper.java
new file mode 100644
index 0000000..f113a85
--- /dev/null
+++ b/libraries/system-helpers/settings-helper/src/android/system/helpers/SettingsHelper.java
@@ -0,0 +1,594 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.system.helpers;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothManager;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.wifi.WifiManager;
+import android.provider.Settings;
+import android.provider.Settings.SettingNotFoundException;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.UiObjectNotFoundException;
+import android.support.test.uiautomator.UiScrollable;
+import android.support.test.uiautomator.UiSelector;
+import android.support.test.uiautomator.Until;
+import android.util.Log;
+import android.widget.Switch;
+import android.widget.TextView;
+
+import junit.framework.Assert;
+
+import java.util.regex.Pattern;
+
+/**
+ * Implement common helper methods for settings.
+ */
+public class SettingsHelper {
+    private static final String TAG = SettingsHelper.class.getSimpleName();
+    private static final String SETTINGS_PACKAGE = "com.android.settings";
+    private static final String SETTINGS_APP = "Settings";
+    private static final String SWITCH_WIDGET = "switch_widget";
+    private static final String WIFI = "Wi-Fi";
+    private static final String BLUETOOTH = "Bluetooth";
+    private static final String AIRPLANE = "Airplane mode";
+    private static final String LOCATION = "Location";
+    private static final String DND = "Do not disturb";
+    private static final String ZEN_MODE = "zen_mode";
+    private static final String FLASHLIGHT = "Flashlight";
+    private static final String AUTO_ROTATE_SCREEN = "Auto-rotate screen";
+    private static final BySelector SETTINGS_DASHBOARD = By.res(SETTINGS_PACKAGE,
+            "dashboard_container");
+    private static final UiSelector LIST_ITEM_VALUE =
+            new UiSelector().className(TextView.class);
+    public static final int TIMEOUT = 2000;
+    private static SettingsHelper sInstance = null;
+    private ActivityHelper mActivityHelper = null;
+    private ContentResolver mResolver = null;
+    private Context mContext = null;
+    private UiDevice mDevice = null;
+
+    public SettingsHelper() {
+        mContext = InstrumentationRegistry.getTargetContext();
+        mResolver = mContext.getContentResolver();
+        mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        mActivityHelper = ActivityHelper.getInstance();
+    }
+
+    public static SettingsHelper getInstance() {
+        if (sInstance == null) {
+            sInstance = new SettingsHelper();
+        }
+        return sInstance;
+    }
+
+    public static enum SettingsType {
+        SYSTEM, SECURE, GLOBAL
+    }
+
+    /**
+     * @return Settings package name
+     */
+    public String getPackage() {
+        return SETTINGS_PACKAGE;
+    }
+
+    /**
+     * @return Settings app name
+     */
+    public String getLauncherName() {
+        return SETTINGS_APP;
+    }
+
+    /**
+     * Scroll through settings page
+     * @param numberOfFlings
+     * @throws Exception
+     */
+    public void scrollThroughSettings(int numberOfFlings) throws Exception {
+        UiObject2 settingsList = loadAllSettings();
+        int count = 0;
+        while (count <= numberOfFlings && settingsList.fling(Direction.DOWN)) {
+            count++;
+        }
+    }
+
+    /**
+     * Move to top of settings page
+     * @throws Exception
+     */
+    public void flingSettingsToStart() throws Exception {
+        UiObject2 settingsList = loadAllSettings();
+        while (settingsList.fling(Direction.UP));
+    }
+
+    /**
+     * Launch specific settings page
+     * @param ctx
+     * @param pageName
+     * @throws Exception
+     */
+    public static void launchSettingsPage(Context ctx, String pageName) throws Exception {
+        Intent intent = new Intent(pageName);
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        ctx.startActivity(intent);
+        Thread.sleep(TIMEOUT * 2);
+    }
+
+    /**
+     * Scroll vertically up or down
+     * @param isUp
+     */
+    public void scrollVert(boolean isUp) {
+        int w = mDevice.getDisplayWidth();
+        int h = mDevice.getDisplayHeight();
+        mDevice.swipe(w / 2, h / 2, w / 2, isUp ? h : 0, 2);
+    }
+
+    /**
+     * On N, the settingsDashboard is initially collapsed, and the user can see the "See all"
+     * element. On hitting "See all", the same settings dashboard element is now scrollable. For
+     * pre-N, the settings Dashboard is always scrollable, hence the check in the while loop. All
+     * this method does is expand the Settings list if needed, before returning the element.
+     */
+    public UiObject2 loadAllSettings() throws Exception {
+        UiObject2 settingsDashboard = mDevice.wait(Until.findObject(SETTINGS_DASHBOARD),
+                TIMEOUT * 2);
+        Assert.assertNotNull("Could not find the settings dashboard object.", settingsDashboard);
+        int count = 0;
+        while (!settingsDashboard.isScrollable() && count <= 2) {
+            mDevice.wait(Until.findObject(By.text("SEE ALL")), TIMEOUT * 2).click();
+            settingsDashboard = mDevice.wait(Until.findObject(SETTINGS_DASHBOARD),
+                    TIMEOUT * 2);
+            count++;
+        }
+        return settingsDashboard;
+    }
+
+    /**
+     * Performs click action on a setting when setting name is provided as exact string
+     * @param settingName
+     * @throws InterruptedException
+     */
+    public void clickSetting(String settingName) throws InterruptedException {
+        int count = 5;
+        while (count > 0 && mDevice.wait(Until.findObject(By.text(settingName)), TIMEOUT) == null) {
+            scrollVert(false);
+            count--;
+        }
+        mDevice.wait(Until.findObject(By.text(settingName)), TIMEOUT).click();
+        Thread.sleep(TIMEOUT);
+    }
+
+    /**
+     * Performs click action on a setting when setting has been found
+     * @param name
+     * @throws InterruptedException,UiObjectNotFoundException
+     */
+    public boolean selectSettingFor(String settingName)
+            throws InterruptedException, UiObjectNotFoundException {
+        UiScrollable settingsList = new UiScrollable(
+                new UiSelector().resourceId("android:id/content"));
+        UiObject appSettings = settingsList.getChildByText(LIST_ITEM_VALUE, settingName);
+        if (appSettings != null) {
+            return appSettings.click();
+        }
+        return false;
+    }
+
+    /**
+     * Performs click action on a setting when setting name is provided as pattern
+     *
+     * @param settingName
+     * @throws InterruptedException
+     */
+    public void clickSetting(Pattern settingName) throws InterruptedException {
+        mDevice.wait(Until.findObject(By.text(settingName)), TIMEOUT).click();
+        Thread.sleep(400);
+    }
+
+    /**
+     * Gets string value of a setting
+     * @param type
+     * @param sName
+     * @return
+     */
+    public String getStringSetting(SettingsType type, String sName) {
+        switch (type) {
+            case SYSTEM:
+                return Settings.System.getString(mResolver, sName);
+            case GLOBAL:
+                return Settings.Global.getString(mResolver, sName);
+            case SECURE:
+                return Settings.Secure.getString(mResolver, sName);
+        }
+        return "";
+    }
+
+    /**
+     * Get int value of a setting
+     * @param type
+     * @param sName
+     * @return
+     * @throws SettingNotFoundException
+     */
+    public int getIntSetting(SettingsType type, String sName) throws SettingNotFoundException {
+        switch (type) {
+            case SYSTEM:
+                return Settings.System.getInt(mResolver, sName);
+            case GLOBAL:
+                return Settings.Global.getInt(mResolver, sName);
+            case SECURE:
+                return Settings.Secure.getInt(mResolver, sName);
+        }
+        return Integer.MIN_VALUE;
+    }
+
+    /**
+     * Set string value of a setting
+     * @param type
+     * @param sName
+     * @param value
+     */
+    public void setStringSetting(SettingsType type, String sName, String value)
+            throws InterruptedException {
+        switch (type) {
+            case SYSTEM:
+                Settings.System.putString(mResolver, sName, value);
+                break;
+            case GLOBAL:
+                Settings.Global.putString(mResolver, sName, value);
+                break;
+            case SECURE:
+                Settings.Secure.putString(mResolver, sName, value);
+                break;
+        }
+        Thread.sleep(TIMEOUT);
+    }
+
+    /**
+     * Sets int value of a setting
+     * @param type
+     * @param sName
+     * @param value
+     */
+    public void setIntSetting(SettingsType type, String sName, int value)
+            throws InterruptedException {
+        switch (type) {
+            case SYSTEM:
+                Settings.System.putInt(mResolver, sName, value);
+                break;
+            case GLOBAL:
+                Settings.Global.putInt(mResolver, sName, value);
+                break;
+            case SECURE:
+                Settings.Secure.putInt(mResolver, sName, value);
+                break;
+        }
+        Thread.sleep(TIMEOUT);
+    }
+
+    /**
+     * Toggles setting and verifies the action, when setting name is passed as string
+     * @param type
+     * @param settingAction
+     * @param settingName
+     * @param internalName
+     * @return
+     * @throws Exception
+     */
+    public boolean verifyToggleSetting(SettingsType type, String settingAction,
+            String settingName, String internalName) throws Exception {
+        return verifyToggleSetting(
+                type, settingAction, Pattern.compile(settingName), internalName, true);
+    }
+
+    /**
+     * Toggles setting and verifies the action, when setting name is passed as pattern
+     * @param type
+     * @param settingAction
+     * @param settingName
+     * @param internalName
+     * @return
+     * @throws Exception
+     */
+    public boolean verifyToggleSetting(SettingsType type, String settingAction,
+            Pattern settingName, String internalName) throws Exception {
+        return verifyToggleSetting(type, settingAction, settingName, internalName, true);
+    }
+
+    /**
+     * Toggles setting and verifies the action, when setting name is passed as string
+     * and settings page needs to be launched or not
+     * @param type
+     * @param settingAction
+     * @param settingName
+     * @param internalName
+     * @param doLaunch
+     * @return
+     * @throws Exception
+     */
+    public boolean verifyToggleSetting(SettingsType type, String settingAction,
+            String settingName, String internalName, boolean doLaunch) throws Exception {
+        return verifyToggleSetting(
+                type, settingAction, Pattern.compile(settingName), internalName, doLaunch);
+    }
+
+    /**
+     * Toggles setting and verifies the action
+     * @param type
+     * @param settingAction
+     * @param settingName
+     * @param internalName
+     * @param doLaunch
+     * @return
+     * @throws Exception
+     */
+    public boolean verifyToggleSetting(SettingsType type, String settingAction,
+            Pattern settingName, String internalName, boolean doLaunch) throws Exception {
+        String onSettingBaseVal = getStringSetting(type, internalName);
+        if (onSettingBaseVal == null) {
+            // Per bug b/35717943 default for charging sounds is ON
+            // So if null, the value should be set to 1.
+            if (settingName.matcher("Charging sounds").matches()) {
+                onSettingBaseVal = "1";
+            }
+            else {
+                onSettingBaseVal = "0";
+            }
+        }
+        int onSetting = Integer.parseInt(onSettingBaseVal);
+        Log.d(TAG, "On Setting value is : " + onSetting);
+        if (doLaunch) {
+            launchSettingsPage(mContext, settingAction);
+        }
+        clickSetting(settingName);
+        Log.d(TAG, "Clicked setting : " + settingName);
+        Thread.sleep(5000);
+        String changedSetting = getStringSetting(type, internalName);
+        Log.d(TAG, "Changed Setting value is : " + changedSetting);
+        if (changedSetting == null) {
+            Log.d(TAG, "Changed Setting value is : NULL");
+            changedSetting = "0";
+        }
+        return (1 - onSetting) == Integer.parseInt(changedSetting);
+    }
+
+    /**
+     * @param type
+     * @param settingAction
+     * @param baseName
+     * @param settingName
+     * @param internalName
+     * @param testVal
+     * @return
+     * @throws Exception
+     */
+    public boolean verifyRadioSetting(SettingsType type, String settingAction,
+            String baseName, String settingName,
+            String internalName, String testVal) throws Exception {
+        if (baseName != null)
+            clickSetting(baseName);
+        clickSetting(settingName);
+        Thread.sleep(500);
+        return getStringSetting(type, internalName).equals(testVal);
+    }
+
+    public void toggleWiFiOnOffAndVerify(boolean verifyOn, boolean isQuickSettings)
+            throws Exception {
+        String switchText = (verifyOn ? "OFF" : "ON");
+        WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
+        wifiManager.setWifiEnabled(!verifyOn);
+        Thread.sleep(TIMEOUT * 3);
+        if (isQuickSettings) {
+            launchAndClickSettings(isQuickSettings, null, By.descContains(WIFI)
+                    .clazz(Switch.class));
+        } else {
+            launchAndClickSettings(isQuickSettings, Settings.ACTION_WIFI_SETTINGS,
+                    By.res(SETTINGS_PACKAGE, SWITCH_WIDGET).text(switchText));
+        }
+        Thread.sleep(TIMEOUT * 3);
+        String wifiValue = Settings.Global.getString(mResolver, Settings.Global.WIFI_ON);
+        if (verifyOn) {
+            Assert.assertFalse(wifiValue == "0");
+        } else {
+            Assert.assertEquals("0", wifiValue);
+        }
+        mDevice.pressHome();
+        Thread.sleep(TIMEOUT * 3);
+    }
+
+    public void toggleBTOnOffAndVerify(boolean verifyOn, boolean isQuickSettings)
+            throws Exception {
+        String switchText = (verifyOn ? "OFF" : "ON");
+        BluetoothAdapter bluetoothAdapter = ((BluetoothManager) mContext
+                .getSystemService(Context.BLUETOOTH_SERVICE)).getAdapter();
+        boolean isEnabled = bluetoothAdapter.isEnabled();
+        boolean success = (verifyOn ? bluetoothAdapter.disable() : bluetoothAdapter.enable());
+        Thread.sleep(TIMEOUT * 3);
+        if (isQuickSettings) {
+            launchAndClickSettings(isQuickSettings, null,
+                    By.descContains(BLUETOOTH).clazz(Switch.class));
+        } else {
+            launchAndClickSettings(isQuickSettings, Settings.ACTION_BLUETOOTH_SETTINGS,
+                    By.res(SETTINGS_PACKAGE, SWITCH_WIDGET).text(switchText));
+        }
+        Thread.sleep(TIMEOUT * 3);
+        String bluetoothValue = Settings.Global.getString(
+                mResolver,
+                Settings.Global.BLUETOOTH_ON);
+        Assert.assertEquals((verifyOn ? "1" : "0"), bluetoothValue);
+        if (isEnabled) {
+            bluetoothAdapter.enable();
+        } else {
+            bluetoothAdapter.disable();
+        }
+        mDevice.pressHome();
+        Thread.sleep(TIMEOUT * 3);
+    }
+
+    public void toggleAirplaneModeOnOrOffAndVerify(boolean verifyOn, boolean isQuickSettings)
+            throws Exception {
+        String settingValToPut = (verifyOn ? "0" : "1");
+        Settings.Global.putString(mResolver, Settings.Global.AIRPLANE_MODE_ON, settingValToPut);
+        if (isQuickSettings) {
+            launchAndClickSettings(isQuickSettings, null, By.descContains(AIRPLANE));
+        } else {
+            launchAndClickSettings(isQuickSettings, Settings.ACTION_WIRELESS_SETTINGS,
+                    By.text(AIRPLANE));
+        }
+        Thread.sleep(TIMEOUT * 3);
+        String airplaneModeValue = Settings.Global
+                .getString(mResolver,
+                        Settings.Global.AIRPLANE_MODE_ON);
+        Assert.assertEquals((verifyOn ? "1" : "0"), airplaneModeValue);
+        mDevice.pressHome();
+        Thread.sleep(TIMEOUT * 3);
+    }
+
+    public void toggleLocationSettingsOnOrOffAndVerify(boolean verifyOn, boolean isQuickSettings)
+            throws Exception {
+        // Set location flag
+        int settingValToPut = (verifyOn ? Settings.Secure.LOCATION_MODE_OFF
+                : Settings.Secure.LOCATION_MODE_SENSORS_ONLY);
+        Settings.Secure.putInt(mResolver, Settings.Secure.LOCATION_MODE, settingValToPut);
+        // Load location settings
+        if (isQuickSettings) {
+            launchAndClickSettings(isQuickSettings, null, By.descContains(LOCATION));
+        } else {
+            launchAndClickSettings(isQuickSettings, Settings.ACTION_LOCATION_SOURCE_SETTINGS,
+                    By.res(SETTINGS_PACKAGE, SWITCH_WIDGET));
+        }
+        Thread.sleep(TIMEOUT * 3);
+        // Verify change in setting
+        int locationEnabled = Settings.Secure.getInt(mResolver,
+                Settings.Secure.LOCATION_MODE);
+        if (verifyOn) {
+            Assert.assertFalse("Location not enabled correctly", locationEnabled == 0);
+        } else {
+            Assert.assertEquals("Location not disabled correctly", 0, locationEnabled);
+        }
+        mDevice.pressHome();
+        Thread.sleep(TIMEOUT * 3);
+    }
+
+    public void launchAndClickSettings(boolean isQuickSettings, String settingsPage,
+            BySelector bySelector) throws Exception {
+        if (isQuickSettings) {
+            launchQuickSettingsAndWait();
+            UiObject2 qsTile = mDevice.wait(Until.findObject(bySelector), TIMEOUT * 3);
+            qsTile.findObject(By.clazz("android.widget.FrameLayout")).click();
+        } else {
+            mActivityHelper.launchIntent(settingsPage);
+            mDevice.wait(Until.findObject(bySelector), TIMEOUT * 3).click();
+        }
+    }
+
+    /**
+     * Verify Quick Setting DND can be toggled DND default value is OFF
+     * @throws Exception
+     */
+    public void toggleQuickSettingDNDAndVerify() throws Exception {
+        try {
+            int onSetting = Settings.Global.getInt(mResolver, ZEN_MODE);
+            launchQuickSettingsAndWait();
+            mDevice.wait(Until.findObject(By.descContains(DND)),
+                    TIMEOUT * 3).getChildren().get(0).click();
+            Thread.sleep(TIMEOUT * 3);
+            int changedSetting = Settings.Global.getInt(mResolver, ZEN_MODE);
+            Assert.assertFalse(onSetting == changedSetting);
+            mDevice.pressHome();
+            Thread.sleep(TIMEOUT * 3);
+        } finally {
+            // change to DND default value
+            int setting = Settings.Global.getInt(mResolver, ZEN_MODE);
+            if (setting > 0) {
+                launchQuickSettingsAndWait();
+                mDevice.wait(Until.findObject(By.descContains(DND)),
+                        TIMEOUT * 3).getChildren().get(0).click();
+                Thread.sleep(TIMEOUT * 3);
+            }
+        }
+    }
+
+    public void toggleQuickSettingFlashLightAndVerify() throws Exception {
+        String lightOn = "On";
+        String lightOff = "Off";
+        boolean verifyOn = false;
+        launchQuickSettingsAndWait();
+        UiObject2 flashLight = mDevice.wait(
+                Until.findObject(By.desc(FLASHLIGHT)),
+                TIMEOUT * 3);
+        if (flashLight != null && flashLight.getText().equals(lightOn)) {
+            verifyOn = true;
+        }
+        mDevice.wait(Until.findObject(By.desc(FLASHLIGHT)),
+                TIMEOUT * 3).click();
+        Thread.sleep(TIMEOUT * 3);
+        flashLight = mDevice.wait(
+                Until.findObject(By.desc(FLASHLIGHT)),
+                TIMEOUT * 3);
+        if (flashLight != null) {
+            String txt = flashLight.getText();
+            if (verifyOn) {
+                Assert.assertTrue(txt.equals(lightOff));
+            } else {
+                Assert.assertTrue(txt.equals(lightOn));
+                mDevice.wait(Until.findObject(By.textContains(FLASHLIGHT)),
+                        TIMEOUT * 3).click();
+            }
+        }
+        mDevice.pressHome();
+        Thread.sleep(TIMEOUT * 3);
+    }
+
+    public void toggleQuickSettingOrientationAndVerify() throws Exception {
+        launchQuickSettingsAndWait();
+        mDevice.wait(Until.findObject(By.descContains(AUTO_ROTATE_SCREEN)),
+                TIMEOUT * 3).click();
+        Thread.sleep(TIMEOUT * 3);
+        String rotation = Settings.System.getString(mResolver,
+                Settings.System.ACCELEROMETER_ROTATION);
+        Assert.assertEquals("1", rotation);
+        mDevice.setOrientationNatural();
+        mDevice.pressHome();
+        Thread.sleep(TIMEOUT * 3);
+    }
+
+    public void launchQuickSettingsAndWait() throws Exception {
+        mDevice.openQuickSettings();
+        Thread.sleep(TIMEOUT * 2);
+    }
+
+    public void launchSettingsPageByComponentName(Context ctx, String name) {
+        Intent intent = new Intent(Intent.ACTION_MAIN);
+        ComponentName settingComponent = new ComponentName(SETTINGS_PACKAGE,
+                String.format("%s.%s$%s", SETTINGS_PACKAGE, SETTINGS_APP, name));
+        intent.setComponent(settingComponent);
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        ctx.startActivity(intent);
+    }
+}
diff --git a/libraries/base-app-helpers/Android.mk b/libraries/system-helpers/sysui-helper/Android.mk
similarity index 79%
copy from libraries/base-app-helpers/Android.mk
copy to libraries/system-helpers/sysui-helper/Android.mk
index 0111a0a..2adcbcd 100644
--- a/libraries/base-app-helpers/Android.mk
+++ b/libraries/system-helpers/sysui-helper/Android.mk
@@ -16,9 +16,14 @@
 LOCAL_PATH := $(call my-dir)
 
 include $(CLEAR_VARS)
-LOCAL_MODULE := base-app-helpers
-LOCAL_STATIC_JAVA_LIBRARIES := dpad-util
-LOCAL_JAVA_LIBRARIES := ub-uiautomator launcher-helper-lib
+LOCAL_MODULE := sysui-helper
+LOCAL_STATIC_JAVA_LIBRARIES := ub-uiautomator \
+    android-support-test \
+    activity-helper \
+    commands-helper \
+    device-helper \
+    legacy-android-test
+
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
 include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/system-helpers/sysui-helper/src/android/system/helpers/HotseatHelper.java b/libraries/system-helpers/sysui-helper/src/android/system/helpers/HotseatHelper.java
new file mode 100644
index 0000000..59e44da
--- /dev/null
+++ b/libraries/system-helpers/sysui-helper/src/android/system/helpers/HotseatHelper.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.system.helpers;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.UiObjectNotFoundException;
+import android.support.test.uiautomator.Until;
+
+import junit.framework.Assert;
+
+/**
+ * Implement common helper methods for Hotseat.
+ */
+public class HotseatHelper {
+    private static final int TIMEOUT = 3000;
+    private UiDevice mDevice = null;
+    private PackageManager mPkgManger = null;
+    private Context mContext = null;
+    public static HotseatHelper sInstance = null;
+
+    private HotseatHelper() {
+        mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        mContext = InstrumentationRegistry.getTargetContext();
+    }
+
+    public static HotseatHelper getInstance() {
+        if (sInstance == null) {
+            sInstance = new HotseatHelper();
+        }
+        return sInstance;
+    }
+
+    /**
+     * Launch app from hotseat
+     * @param textAppName
+     * @param appPackage
+     */
+    public void launchAppFromHotseat(String textAppName, String appPackage) {
+        mDevice.pressHome();
+        UiObject2 appOnHotseat = mDevice.findObject(By.clazz("android.widget.TextView")
+                .desc(textAppName));
+        Assert.assertNotNull(textAppName + " app couldn't be found on hotseat", appOnHotseat);
+        appOnHotseat.click();
+        UiObject2 appLoaded = mDevice.wait(Until.findObject(By.pkg(appPackage)), TIMEOUT * 2);
+        Assert.assertNotNull(textAppName + "app did not load on tapping from hotseat", appLoaded);
+    }
+}
\ No newline at end of file
diff --git a/libraries/system-helpers/sysui-helper/src/android/system/helpers/LockscreenHelper.java b/libraries/system-helpers/sysui-helper/src/android/system/helpers/LockscreenHelper.java
new file mode 100644
index 0000000..ece416b
--- /dev/null
+++ b/libraries/system-helpers/sysui-helper/src/android/system/helpers/LockscreenHelper.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.system.helpers;
+
+import android.app.KeyguardManager;
+import android.content.Context;
+import android.provider.Settings;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.system.helpers.ActivityHelper;
+
+import junit.framework.Assert;
+
+import java.io.IOException;
+/**
+ * Implement common helper methods for Lockscreen.
+ */
+public class LockscreenHelper {
+    private static final String LOG_TAG = LockscreenHelper.class.getSimpleName();
+    public static final int SHORT_TIMEOUT = 200;
+    public static final int LONG_TIMEOUT = 2000;
+    public static final String EDIT_TEXT_CLASS_NAME = "android.widget.EditText";
+    public static final String CAMERA2_PACKAGE = "com.android.camera2";
+    public static final String CAMERA_PACKAGE = "com.google.android.GoogleCamera";
+    public static final String MODE_PIN = "PIN";
+    private static final int SWIPE_MARGIN = 5;
+    private static final int DEFAULT_FLING_STEPS = 5;
+    private static final int DEFAULT_SCROLL_STEPS = 15;
+
+    private static final String SET_PIN_COMMAND = "locksettings set-pin %s";
+    private static final String CLEAR_COMMAND = "locksettings clear --old %s";
+
+    private static LockscreenHelper sInstance = null;
+    private Context mContext = null;
+    private UiDevice mDevice = null;
+    private final ActivityHelper mActivityHelper;
+    private final CommandsHelper mCommandsHelper;
+    private final DeviceHelper mDeviceHelper;
+    private boolean mIsRyuDevice = false;
+
+    public LockscreenHelper() {
+        mContext = InstrumentationRegistry.getTargetContext();
+        mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        mActivityHelper = ActivityHelper.getInstance();
+        mCommandsHelper = CommandsHelper.getInstance(InstrumentationRegistry.getInstrumentation());
+        mDeviceHelper = DeviceHelper.getInstance();
+        mIsRyuDevice = mDeviceHelper.isRyuDevice();
+
+    }
+
+    public static LockscreenHelper getInstance() {
+        if (sInstance == null) {
+            sInstance = new LockscreenHelper();
+        }
+        return sInstance;
+    }
+
+    /**
+     * Launch Camera on LockScreen
+     * @return true/false
+     */
+    public boolean launchCameraOnLockScreen() {
+        String CameraPackage = mIsRyuDevice ? CAMERA2_PACKAGE : CAMERA_PACKAGE;
+        int w = mDevice.getDisplayWidth();
+        int h = mDevice.getDisplayHeight();
+        // Load camera on LockScreen and take a photo
+        mDevice.drag((w - 25), (h - 25), (int) (w * 0.5), (int) (w * 0.5), 40);
+        mDevice.waitForIdle();
+        return mDevice.wait(Until.hasObject(
+                By.res(CameraPackage, "activity_root_view")),
+                LONG_TIMEOUT * 2);
+    }
+
+     /**
+     * Sets the screen lock pin or password
+     * @param pwd text of Password or Pin for lockscreen
+     * @param mode indicate if its password or PIN
+     * @throws InterruptedException
+     */
+    public void setScreenLock(String pwd, String mode, boolean mIsNexusDevice) throws InterruptedException {
+        navigateToScreenLock();
+        mDevice.wait(Until.findObject(By.text(mode)), LONG_TIMEOUT * 2).click();
+        // set up Secure start-up page
+        if (!mIsNexusDevice) {
+            mDevice.wait(Until.findObject(By.text("No thanks")), LONG_TIMEOUT).click();
+        }
+        UiObject2 pinField = mDevice.wait(Until.findObject(By.clazz(EDIT_TEXT_CLASS_NAME)),
+                LONG_TIMEOUT);
+        pinField.setText(pwd);
+        // enter and verify password
+        mDevice.pressEnter();
+        pinField.setText(pwd);
+        mDevice.pressEnter();
+        mDevice.wait(Until.findObject(By.text("DONE")), LONG_TIMEOUT).click();
+    }
+
+    /**
+     * check if Emergency Call page exists
+     * @throws InterruptedException
+     */
+    public void checkEmergencyCallOnLockScreen() throws InterruptedException {
+        mDevice.pressMenu();
+        mDevice.wait(Until.findObject(By.text("EMERGENCY")), LONG_TIMEOUT).click();
+        Thread.sleep(LONG_TIMEOUT);
+        UiObject2 dialButton = mDevice.wait(Until.findObject(By.desc("dial")),
+                LONG_TIMEOUT);
+        Assert.assertNotNull("Can't reach emergency call page", dialButton);
+        mDevice.pressBack();
+        Thread.sleep(LONG_TIMEOUT);
+    }
+
+    /**
+     * remove Screen Lock
+     * @throws InterruptedException
+     */
+    public void removeScreenLock(String pwd)
+            throws InterruptedException {
+        navigateToScreenLock();
+        UiObject2 pinField = mDevice.wait(Until.findObject(By.clazz(EDIT_TEXT_CLASS_NAME)),
+                LONG_TIMEOUT);
+        pinField.setText(pwd);
+        mDevice.pressEnter();
+        mDevice.wait(Until.findObject(By.text("Swipe")), LONG_TIMEOUT).click();
+        mDevice.waitForIdle();
+        mDevice.wait(Until.findObject(By.text("YES, REMOVE")), LONG_TIMEOUT).click();
+    }
+
+    /**
+     * unlock screen
+     * @throws InterruptedException, IOException
+     */
+    public void unlockScreen(String pwd)
+            throws InterruptedException, IOException {
+        String command = String.format(" %s %s %s", "input", "keyevent", "82");
+        mDevice.executeShellCommand(command);
+        Thread.sleep(SHORT_TIMEOUT);
+        Thread.sleep(SHORT_TIMEOUT);
+        // enter password to unlock screen
+        command = String.format(" %s %s %s", "input", "text", pwd);
+        mDevice.executeShellCommand(command);
+        mDevice.waitForIdle();
+        Thread.sleep(SHORT_TIMEOUT);
+        mDevice.pressEnter();
+    }
+
+    /**
+     * navigate to screen lock setting page
+     * @throws InterruptedException
+     */
+    public void navigateToScreenLock()
+            throws InterruptedException {
+        mActivityHelper.launchIntent(Settings.ACTION_SECURITY_SETTINGS);
+        mDevice.wait(Until.findObject(By.text("Screen lock")), LONG_TIMEOUT).click();
+    }
+
+    /**
+     * check if Lock Screen is enabled
+     */
+    public boolean isLockScreenEnabled() {
+        KeyguardManager km = (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE);
+        return km.isKeyguardSecure();
+    }
+
+    /**
+     * Sets a screen lock via shell.
+     */
+    public void setScreenLockViaShell(String pwd, String mode) throws Exception {
+        switch (mode) {
+            case MODE_PIN:
+                mCommandsHelper.executeShellCommand(String.format(SET_PIN_COMMAND, pwd));
+                break;
+            default:
+                throw new IllegalArgumentException("Unsupported mode: " + mode);
+        }
+    }
+
+    /**
+     * Removes the screen lock via shell.
+     */
+    public void removeScreenLockViaShell(String pwd) throws Exception {
+        mCommandsHelper.executeShellCommand(String.format(CLEAR_COMMAND, pwd));
+    }
+
+    /**
+     * swipe up to unlock the screen
+     */
+    public void unlockScreenSwipeUp() throws Exception {
+        mDevice.wakeUp();
+        mDevice.waitForIdle();
+        mDevice.swipe(mDevice.getDisplayWidth() / 2,
+                mDevice.getDisplayHeight() - SWIPE_MARGIN,
+                mDevice.getDisplayWidth() / 2,
+                SWIPE_MARGIN,
+                DEFAULT_SCROLL_STEPS);
+        mDevice.waitForIdle();
+    }
+}
\ No newline at end of file
diff --git a/libraries/system-helpers/sysui-helper/src/android/system/helpers/NotificationHelper.java b/libraries/system-helpers/sysui-helper/src/android/system/helpers/NotificationHelper.java
new file mode 100644
index 0000000..7554b60
--- /dev/null
+++ b/libraries/system-helpers/sysui-helper/src/android/system/helpers/NotificationHelper.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.system.helpers;
+
+import android.app.IntentService;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.RemoteInput;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+import android.service.notification.StatusBarNotification;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.view.KeyEvent;
+import android.view.inputmethod.InputMethodManager;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.util.Log;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.Toast;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Implement common helper methods for Notification.
+ */
+public class NotificationHelper {
+    private static final String LOG_TAG = NotificationHelper.class.getSimpleName();
+    public static final int SHORT_TIMEOUT = 200;
+    public static final int LONG_TIMEOUT = 2000;
+    private static NotificationHelper sInstance = null;
+    private Context mContext = null;
+    private UiDevice mDevice = null;
+
+    public NotificationHelper() {
+        mContext = InstrumentationRegistry.getTargetContext();
+        mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+    }
+
+    public static NotificationHelper getInstance() {
+        if (sInstance == null) {
+            sInstance = new NotificationHelper();
+        }
+        return sInstance;
+    }
+
+    /**
+     * check if notification exists
+     * @param id notification id
+     * @param mNotificationManager NotificationManager
+     * @return true/false
+     * @throws Exception
+     */
+    public boolean checkNotificationExistence(int id, NotificationManager mNotificationManager)
+            throws Exception {
+        boolean isFound = false;
+        for (int tries = 3; tries-- > 0;) {
+            isFound = false;
+            StatusBarNotification[] sbns = mNotificationManager.getActiveNotifications();
+            for (StatusBarNotification sbn : sbns) {
+                if (sbn.getId() == id) {
+                    isFound = true;
+                    break;
+                }
+            }
+            if (isFound) {
+                break;
+            }
+            Thread.sleep(SHORT_TIMEOUT);
+        }
+        Log.i(LOG_TAG, "checkNotificationExistence..." + isFound);
+        return isFound;
+    }
+
+    /**
+     * send out a group of notifications
+     * @param lists notification list for a group of notifications which includes two child
+     *            notifications and one summary notification
+     * @param groupKey the group key of group notification
+     * @param mNotificationManager NotificationManager
+     * @throws Exception
+     */
+    public void sendBundlingNotifications(List<Integer> lists, String groupKey,
+            NotificationManager mNotificationManager) throws Exception {
+        Notification childNotification = new Notification.Builder(mContext)
+                .setContentTitle(lists.get(1).toString())
+                .setSmallIcon(android.R.drawable.stat_notify_chat)
+                .setContentText("test1")
+                .setWhen(System.currentTimeMillis())
+                .setGroup(groupKey)
+                .build();
+        mNotificationManager.notify(lists.get(1),
+                childNotification);
+        childNotification = new Notification.Builder(mContext)
+                .setContentTitle(lists.get(2).toString())
+                .setContentText("test2")
+                .setSmallIcon(android.R.drawable.stat_notify_chat)
+                .setWhen(System.currentTimeMillis())
+                .setGroup(groupKey)
+                .build();
+        mNotificationManager.notify(lists.get(2),
+                childNotification);
+        Notification notification = new Notification.Builder(mContext)
+                .setContentTitle(lists.get(0).toString())
+                .setSubText(groupKey)
+                .setSmallIcon(android.R.drawable.stat_notify_chat)
+                .setGroup(groupKey)
+                .setGroupSummary(true)
+                .build();
+        mNotificationManager.notify(lists.get(0),
+                notification);
+    }
+
+    /**
+     * send out a notification with inline reply
+     * @param notificationId An identifier for this notification
+     * @param title notification title
+     * @param inLineReply inline reply text
+     * @param mNotificationManager NotificationManager
+     */
+    public void sendNotificationsWithInLineReply(int notificationId, String title,
+            String inLineReply,PendingIntent pendingIntent, NotificationManager mNotificationManager) {
+        Notification.Action action = new Notification.Action.Builder(
+                android.R.drawable.stat_notify_chat, "Reply",
+                pendingIntent).addRemoteInput(new RemoteInput.Builder(inLineReply)
+                        .setLabel("Quick reply").build())
+                        .build();
+        Notification.Builder n = new Notification.Builder(mContext)
+                .setContentTitle(Integer.toString(notificationId))
+                .setContentText(title)
+                .setWhen(System.currentTimeMillis())
+                .setSmallIcon(android.R.drawable.stat_notify_chat)
+                .addAction(action)
+                .setDefaults(Notification.DEFAULT_VIBRATE);
+        mNotificationManager.notify(notificationId, n.build());
+    }
+
+    /**
+     * dismiss notification
+     * @param mNotificationManager NotificationManager
+     */
+    public void dismissNotifications(NotificationManager mNotificationManager){
+        mNotificationManager.cancelAll();
+    }
+
+    /**
+     * open notification shade
+     */
+    public void openNotification(){
+        mDevice.openNotification();
+    }
+}
\ No newline at end of file
diff --git a/libraries/system-helpers/sysui-helper/src/android/system/helpers/OverviewHelper.java b/libraries/system-helpers/sysui-helper/src/android/system/helpers/OverviewHelper.java
new file mode 100644
index 0000000..39feb94
--- /dev/null
+++ b/libraries/system-helpers/sysui-helper/src/android/system/helpers/OverviewHelper.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.system.helpers;
+
+import android.app.Instrumentation;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.SystemClock;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.UiObjectNotFoundException;
+import android.support.test.uiautomator.Until;
+import android.system.helpers.ActivityHelper;
+import android.util.Log;
+
+import org.junit.Assert;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Implement common helper methods for Overview.
+ */
+public class OverviewHelper {
+
+    private static final String TAG = OverviewHelper.class.getSimpleName();
+    private static final int TIMEOUT = 3000;
+    private static final String RECENTS = "com.android.systemui:id/recents_view";
+
+    private UiDevice mDevice = null;
+    private Instrumentation mInstrumentation = null;
+    private ActivityHelper mActHelper = null;
+    private final CommandsHelper mCommandsHelper;
+    public static OverviewHelper sInstance = null;
+
+    public OverviewHelper() {
+        mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        mInstrumentation = InstrumentationRegistry.getInstrumentation();
+        mActHelper = ActivityHelper.getInstance();
+        mCommandsHelper = CommandsHelper.getInstance(mInstrumentation);
+    }
+
+    public static OverviewHelper getInstance() {
+        if (sInstance == null) {
+            sInstance = new OverviewHelper();
+        }
+        return sInstance;
+    }
+
+    /**
+     * Navigates to the recents screen
+     * @returns recents object
+     * @throws UiObjectNotFoundException
+     */
+    public UiObject2 navigateToRecents() throws Exception {
+        mDevice.pressRecentApps();
+        mDevice.waitForIdle();
+        return mDevice.wait(Until.findObject(By.res(RECENTS)), TIMEOUT);
+    }
+
+    /**
+     * Populates recents by launching six apps
+     * @throws InterruptedException
+     */
+    public void populateRecents() throws InterruptedException {
+        // We launch six apps, since five is the maximum number
+        // of apps under Recents
+        String[] appPackages = {"com.google.android.gm",
+                "com.google.android.deskclock", "com.android.settings",
+                "com.google.android.youtube", "com.google.android.contacts",
+                "com.google.android.apps.maps"};
+        for (String appPackage : appPackages) {
+            mActHelper.launchPackage(appPackage);
+        }
+    }
+
+    public ArrayList<String> populateManyRecentApps() throws IOException {
+        PackageManager pm = mInstrumentation.getContext().getPackageManager();
+        List<PackageInfo> packages = pm.getInstalledPackages(0);
+        ArrayList<String> launchedPackages = new ArrayList<>();
+        for (PackageInfo pkg : packages) {
+            if (pkg.packageName.equals(mInstrumentation.getTargetContext().getPackageName())) {
+                continue;
+            }
+            Intent intent = pm.getLaunchIntentForPackage(pkg.packageName);
+            if (intent == null) {
+                continue;
+            }
+            intent.addCategory(Intent.CATEGORY_LAUNCHER);
+            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            try {
+                mInstrumentation.getTargetContext().startActivity(intent);
+            } catch (SecurityException e) {
+                Log.i(TAG, "Failed to start package " + pkg.packageName + ", exception: " + e);
+            }
+
+            // Don't overload the system
+            SystemClock.sleep(500);
+            launchedPackages.add(pkg.packageName);
+        }
+
+        // Give the apps some time to finish starting. Some apps start another activity while
+        // starting, and we don't want to happen when we are testing stuff.
+        SystemClock.sleep(3000);
+
+        // Close any crash dialogs
+        while (mDevice.hasObject(By.textContains("has stopped"))) {
+            UiObject2 crashDialog = mDevice.findObject(By.text("Close"));
+            if (crashDialog != null) {
+                crashDialog.clickAndWait(Until.newWindow(), 2000);
+            }
+        }
+        return launchedPackages;
+    }
+
+    public void forceStopPackages(ArrayList<String> packages) {
+        for (String pkg : packages) {
+            mCommandsHelper.executeShellCommand("am force-stop " + pkg);
+        }
+    }
+
+    /**
+    * Scrolls through given recents object to the top
+    * @param recents Recents object
+    */
+    public void scrollToTopOfRecents(UiObject2 recents) {
+        Rect r = recents.getVisibleBounds();
+        // decide the top & bottom edges for scroll gesture
+        int top = r.top + r.height() / 4; // top edge = top + 25% height
+        int bottom = r.bottom - 200; // bottom edge = bottom & shift up 200px
+        mDevice.swipe(r.width() / 2, top, r.width() / 2, bottom, 5);
+        mDevice.waitForIdle();
+    }
+
+    /**
+     * Docks an app to the top half of the multiwindow screen
+     * @param appPackageName name of app package
+     * @param appName Name of app to verify on screen
+     * @throws UiObjectNotFoundException, InterruptedException
+     */
+    public void dockAppToTopMultiwindowSlot(String appPackageName, String appName)
+            throws Exception {
+        mDevice.pressRecentApps();
+        mDevice.waitForIdle();
+        UiObject2 recentsView = mDevice.wait(Until.findObject
+                (By.res("com.android.systemui:id/recents_view")),TIMEOUT);
+        // Check if recents isn't already empty, if not, clear it.
+        if (!mDevice.wait(Until.hasObject(By.text("No recent items")),TIMEOUT)) {
+            scrollToTopOfRecents(recentsView);
+            // click clear all
+            UiObject2 clearAll = mDevice.wait(Until.findObject(By.text("CLEAR ALL")),TIMEOUT);
+            if (!clearAll.equals(null)) {
+                clearAll.click();
+            }
+            Thread.sleep(TIMEOUT);
+        }
+        // Open app
+        mActHelper.launchPackage(appPackageName);
+        // Go to overview
+        mDevice.pressRecentApps();
+        mDevice.waitForIdle();
+        // Long press on app
+        UiObject2 appObject = mDevice.wait(Until.findObject
+                (By.desc(appName)),TIMEOUT);
+        int yCoordinate = mDevice.getDisplayHeight() / 12;
+        int xCoordinate = mDevice.getDisplayWidth() / 2;
+        // Drag and drop the app object to the multiwindow area
+        appObject.drag(new Point(xCoordinate, yCoordinate), 1000);
+        // Adding a sleep to allow the drag and drop animation to finish.
+        Thread.sleep(TIMEOUT);
+        mDevice.click(mDevice.getDisplayHeight() / 4, mDevice.getDisplayWidth() / 2);
+        Assert.assertTrue("App not correctly docked to top multiwindow slot",
+                mDevice.wait(Until.hasObject(By.pkg(appPackageName)
+                        .res("android:id/content")), TIMEOUT));
+    }
+
+    /**
+     * Docks two apps, one to the each half of the multiwindow screen
+     * @param topAppPackageName name of app package for top half
+     * @param topAppName Name of top app to verify on screen
+     * @param bottomAppPackageName name of app package for bottom half
+     * @throws UiObjectNotFoundException, InterruptedException
+     */
+    public void dockAppsToBothMultiwindowAreas(String topAppPackageName,
+            String topAppName, String bottomAppPackageName) throws Exception {
+        dockAppToTopMultiwindowSlot(topAppPackageName, topAppName);
+        mDevice.pressHome();
+        mDevice.waitForIdle();
+        // After docking the top app, simply launching another app
+        // will launch it in the bottom half in docked mode. This
+        // results in two apps being docked to multiwindow.
+        mActHelper.launchPackage(bottomAppPackageName);
+    }
+
+    /**
+     * Undocks apps from multiwindow. Only the package for the upper app is needed.
+     * @param topAppPackageName name of app package for top half
+     * @throws UiObjectNotFoundException, InterruptedException
+     */
+    public void undockAppFromMultiwindow(String topAppPackageName) throws Exception {
+        mDevice.click(mDevice.getDisplayHeight() / 4, mDevice.getDisplayWidth() / 2);
+        UiObject2 appArea = mDevice.wait(Until.findObject(By.pkg(topAppPackageName)
+                .res("android:id/content")), TIMEOUT);
+        Rect appBounds = appArea.getVisibleBounds();
+        int xCoordinate = mDevice.getDisplayWidth() / 2;
+        mDevice.drag(xCoordinate, appBounds.bottom, xCoordinate,
+                mDevice.getDisplayHeight() - 120, 4);
+        // Adding a sleep to allow the drag and drop animation to finish.
+        Thread.sleep(TIMEOUT);
+    }
+}
\ No newline at end of file
diff --git a/libraries/system-helpers/sysui-helper/src/android/system/helpers/QuickSettingsHelper.java b/libraries/system-helpers/sysui-helper/src/android/system/helpers/QuickSettingsHelper.java
new file mode 100644
index 0000000..4955cd3
--- /dev/null
+++ b/libraries/system-helpers/sysui-helper/src/android/system/helpers/QuickSettingsHelper.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.system.helpers;
+
+import android.app.Instrumentation;
+import android.content.ContentResolver;
+import android.graphics.Point;
+import android.provider.Settings;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+
+import org.junit.Assert;
+
+/**
+ * Implement common helper methods for Quick settings.
+ */
+public class QuickSettingsHelper {
+
+    private UiDevice mDevice = null;
+    private ContentResolver mResolver;
+    private Instrumentation mInstrumentation;
+    private static final int LONG_TIMEOUT = 2000;
+    private static final int SHORT_TIMEOUT = 500;
+
+    public QuickSettingsHelper(UiDevice device, Instrumentation inst, ContentResolver resolver) {
+        this.mDevice = device;
+        mInstrumentation = inst;
+        mResolver = resolver;
+    }
+
+    public enum QuickSettingDefaultTiles {
+        WIFI("Wi-Fi"), SIM("Mobile data"), DND("Do not disturb"), FLASHLIGHT("Flashlight"), SCREEN(
+                "Auto-rotate screen"), BLUETOOTH("Bluetooth"), AIRPLANE("Airplane mode"),
+                BRIGHTNESS("Display brightness");
+
+        private final String name;
+
+        private QuickSettingDefaultTiles(String name) {
+            this.name = name;
+        }
+
+        public String getName() {
+            return this.name;
+        }
+    };
+
+    public enum QuickSettingEditMenuTiles {
+        LOCATION("Location"), HOTSPOT("Hotspot"), INVERTCOLORS("Invert colors"),
+                DATASAVER("Data Saver"), CAST("Cast"), NEARBY("Nearby");
+
+        private final String name;
+
+        private QuickSettingEditMenuTiles(String name) {
+            this.name = name;
+        }
+
+        public String getName() {
+            return this.name;
+        }
+    };
+
+    public void addQuickSettingTileFromEditMenu(String quickSettingTile,
+            String quickSettingTileToReplace, String quickSettingTileToCheckForInCSV)
+            throws Exception {
+        // Draw down quick settings
+        launchQuickSetting();
+        // Press Edit button
+        UiObject2 quickSettingEdit = mDevice.wait(Until.findObject
+                (By.descContains("Edit")), LONG_TIMEOUT);
+        quickSettingEdit.click();
+        // Scroll down to bottom to see all QS options on Edit
+        swipeDown();
+        // Drag and drop QS item onto existing QS tile to replace it
+        // This is because we need specific coordinates on which to
+        // drop the quick setting tile.
+        UiObject2 quickSettingTileObject = mDevice.wait(Until.findObject
+                (By.descContains(quickSettingTile)), LONG_TIMEOUT);
+        Point destination = mDevice.wait(Until.findObject
+                (By.descContains(quickSettingTileToReplace)), LONG_TIMEOUT)
+                .getVisibleCenter();
+        Assert.assertNotNull(quickSettingTile + " in Edit menu can't be found",
+                quickSettingTileObject);
+        Assert.assertNotNull(quickSettingTileToReplace + " in QS menu can't be found",
+                destination);
+        // Long press the icon, then drag it to the destination slowly.
+        // Without the long press, it ends up scrolling down quick settings.
+        quickSettingTileObject.click(2000);
+        quickSettingTileObject.drag(destination, 1000);
+        // Hit the back button in the QS menu to go back to quick settings.
+        mDevice.wait(Until.findObject(By.descContains("Navigate up")), LONG_TIMEOUT);
+        // Retrieve the quick settings CSV string and verify that the newly
+        // added item is present.
+        String quickSettingsList = Settings.Secure.getString
+                (mInstrumentation.getContext().getContentResolver(),
+                "sysui_qs_tiles");
+        Assert.assertTrue(quickSettingTile + " not present in qs tiles after addition.",
+                quickSettingsList.contains(quickSettingTileToCheckForInCSV));
+    }
+
+    public void setQuickSettingsDefaultTiles() throws Exception {
+        modifyListOfQuickSettingsTiles
+                ("wifi,cell,battery,dnd,flashlight,rotation,bt,airplane,location");
+    }
+
+    public void modifyListOfQuickSettingsTiles(String commaSeparatedList) throws Exception {
+        Settings.Secure.putString(mInstrumentation.getContext().getContentResolver(),
+                "sysui_qs_tiles", commaSeparatedList);
+        Thread.sleep(LONG_TIMEOUT);
+    }
+
+    public void launchQuickSetting() throws Exception {
+        mDevice.pressHome();
+        swipeDown();
+        Thread.sleep(LONG_TIMEOUT);
+        swipeDown();
+    }
+
+    public void swipeUp() throws Exception {
+        mDevice.swipe(mDevice.getDisplayWidth() / 2, mDevice.getDisplayHeight(),
+                mDevice.getDisplayWidth() / 2, 0, 30);
+        Thread.sleep(SHORT_TIMEOUT);
+    }
+
+    public void swipeDown() throws Exception {
+        mDevice.swipe(mDevice.getDisplayWidth() / 2, 0, mDevice.getDisplayWidth() / 2,
+                mDevice.getDisplayHeight() / 2 + 50, 20);
+        Thread.sleep(SHORT_TIMEOUT);
+    }
+
+    public void swipeLeft() {
+        mDevice.swipe(mDevice.getDisplayWidth() / 2, mDevice.getDisplayHeight() / 2, 0,
+                mDevice.getDisplayHeight() / 2, 5);
+    }
+}
diff --git a/libraries/base-app-helpers/Android.mk b/libraries/system-helpers/user-helper/Android.mk
similarity index 84%
copy from libraries/base-app-helpers/Android.mk
copy to libraries/system-helpers/user-helper/Android.mk
index 0111a0a..b0158db 100644
--- a/libraries/base-app-helpers/Android.mk
+++ b/libraries/system-helpers/user-helper/Android.mk
@@ -16,9 +16,10 @@
 LOCAL_PATH := $(call my-dir)
 
 include $(CLEAR_VARS)
-LOCAL_MODULE := base-app-helpers
-LOCAL_STATIC_JAVA_LIBRARIES := dpad-util
-LOCAL_JAVA_LIBRARIES := ub-uiautomator launcher-helper-lib
+LOCAL_MODULE := user-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator \
+    android-support-test \
+    commands-helper
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
 include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/system-helpers/user-helper/src/android/system/helpers/UserHelper.java b/libraries/system-helpers/user-helper/src/android/system/helpers/UserHelper.java
new file mode 100644
index 0000000..55f2904
--- /dev/null
+++ b/libraries/system-helpers/user-helper/src/android/system/helpers/UserHelper.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.system.helpers;
+
+import android.content.Context;
+import android.os.UserManager;
+import android.support.test.InstrumentationRegistry;
+import android.system.helpers.CommandsHelper;
+import android.util.Log;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import junit.framework.Assert;
+
+/**
+ * Implement common helper methods for user.
+ */
+public class UserHelper {
+    private static final String TAG = UserHelper.class.getSimpleName();
+    private static UserHelper sInstance = null;
+    private Context mContext = null;
+
+    public static final int INVALID_USER_ID = -1;
+
+
+    public UserHelper() {
+        mContext = InstrumentationRegistry.getTargetContext();
+    }
+
+    public static UserHelper getInstance() {
+        if (sInstance == null) {
+            sInstance = new UserHelper();
+        }
+        return sInstance;
+    }
+
+    public UserManager getUserManager() {
+        return (UserManager)mContext.getSystemService(Context.USER_SERVICE);
+    }
+
+    /**
+     * Creates a test user
+     * @return id for created secondary user
+     */
+    public int createSecondaryUser(String userName) {
+        // Create user
+        String cmdOut = CommandsHelper.execute("pm create-user " + userName);
+        // Find user id from user-create output
+        // output format : "Success: created user id 10"
+        final Pattern pattern = Pattern.compile("Success: created user id (\\d+)");
+        Matcher matcher = pattern.matcher(cmdOut);
+        int userId = INVALID_USER_ID;
+        if (matcher.find()) {
+            userId = Integer.parseInt(matcher.group(1));
+            Log.i(TAG, String.format("User Name:%s User ID:%d", userName, userId));
+        }
+        return userId;
+    }
+
+    /**
+     * Returns id for first secondary user
+     * @return userid
+     */
+    public int getSecondaryUserId() {
+        String cmdOut = CommandsHelper.execute("pm list users");
+        // Assume that the a user with ID 0 is a primary user. Otherwise secondary users
+        final Pattern USERS_REGEX = Pattern.compile("UserInfo\\{([1-9]\\d*):[\\w\\s]+:(\\d+)\\}");
+        Matcher matcher = USERS_REGEX.matcher(cmdOut);
+        int userId = INVALID_USER_ID;
+        if (matcher.find()) {
+            userId = Integer.parseInt(matcher.group(1));    // 1 = id 2 = flag
+            Log.i(TAG, String.format("The userId is %d", userId));
+        }
+        return userId;
+    }
+
+    public void removeSecondaryUser(int userId) {
+        int prevUserCount = getUserCount();
+        CommandsHelper.execute("pm remove-user " + userId);
+        Assert.assertTrue("User hasn't been removed", getUserCount() == (prevUserCount - 1));
+    }
+
+    public int getUserCount() {
+        return getUserManager().getUserCount();
+    }
+}
\ No newline at end of file
diff --git a/scripts/perf-setup/angler-setup.sh b/scripts/perf-setup/angler-setup.sh
index 7080df7..cd01184 100755
--- a/scripts/perf-setup/angler-setup.sh
+++ b/scripts/perf-setup/angler-setup.sh
@@ -5,24 +5,35 @@
 stop thermal-engine
 stop perfd
 
-echo -n 0 > /sys/devices/system/cpu/cpu0/online
-echo -n 0 > /sys/devices/system/cpu/cpu1/online
-echo -n 0 > /sys/devices/system/cpu/cpu2/online
-echo -n 0 > /sys/devices/system/cpu/cpu3/online
+cpubase=/sys/devices/system/cpu
+gov=cpufreq/scaling_governor
 
-echo -n 1 > /sys/devices/system/cpu/cpu4/online
-echo -n performance > /sys/devices/system/cpu/cpu4/cpufreq/scaling_governor
+cpu=0
+S=960000
+while [ $((cpu < 4)) -eq 1 ]; do
+    echo 1 > $cpubase/cpu${cpu}/online
+    echo userspace > $cpubase/cpu${cpu}/$gov
+    echo $S > $cpubase/cpu${cpu}/cpufreq/scaling_max_freq
+    echo $S > $cpubase/cpu${cpu}/cpufreq/scaling_min_freq
+    echo $S > $cpubase/cpu${cpu}/cpufreq/scaling_setspeed
+    cpu=$(($cpu + 1))
+done
 
-echo -n 1 > /sys/devices/system/cpu/cpu5/online
-echo -n performance > /sys/devices/system/cpu/cpu5/cpufreq/scaling_governor
-
+echo -n 0 > /sys/devices/system/cpu/cpu4/online
+echo -n 0 > /sys/devices/system/cpu/cpu5/online
 echo -n 0 > /sys/devices/system/cpu/cpu6/online
 echo -n 0 > /sys/devices/system/cpu/cpu7/online
 
-echo performance > /sys/class/kgsl/kgsl-3d0/devfreq/governor
-
 echo 0 > /sys/class/kgsl/kgsl-3d0/bus_split
 echo 1 > /sys/class/kgsl/kgsl-3d0/force_clk_on
-
 echo 10000 > /sys/class/kgsl/kgsl-3d0/idle_timer
 
+echo 11863 > /sys/class/devfreq/qcom,gpubw.70/min_freq
+
+echo performance > /sys/class/kgsl/kgsl-3d0/devfreq/governor
+echo 305000000 > /sys/class/kgsl/kgsl-3d0/devfreq/min_freq
+echo 305000000 > /sys/class/kgsl/kgsl-3d0/devfreq/max_freq
+
+echo 4 > /sys/class/kgsl/kgsl-3d0/min_pwrlevel
+echo 4 > /sys/class/kgsl/kgsl-3d0/max_pwrlevel
+
diff --git a/scripts/perf-setup/bullhead-setup.sh b/scripts/perf-setup/bullhead-setup.sh
new file mode 100644
index 0000000..dfecb7d
--- /dev/null
+++ b/scripts/perf-setup/bullhead-setup.sh
@@ -0,0 +1,36 @@
+if [[ "`id -u`" -ne "0" ]]; then
+  echo "WARNING: running as non-root, proceeding anyways..."
+fi
+
+stop thermal-engine
+stop perfd
+
+cpubase=/sys/devices/system/cpu
+gov=cpufreq/scaling_governor
+
+cpu=0
+S=960000
+while [ $((cpu < 4)) -eq 1 ]; do
+    echo 1 > $cpubase/cpu${cpu}/online
+    echo userspace > $cpubase/cpu${cpu}/$gov
+    echo $S > $cpubase/cpu${cpu}/cpufreq/scaling_max_freq
+    echo $S > $cpubase/cpu${cpu}/cpufreq/scaling_min_freq
+    echo $S > $cpubase/cpu${cpu}/cpufreq/scaling_setspeed
+    cpu=$(($cpu + 1))
+done
+
+echo -n 0 > /sys/devices/system/cpu/cpu4/online
+echo -n 0 > /sys/devices/system/cpu/cpu5/online
+
+echo 0 > /sys/class/kgsl/kgsl-3d0/bus_split
+echo 1 > /sys/class/kgsl/kgsl-3d0/force_clk_on
+echo 10000 > /sys/class/kgsl/kgsl-3d0/idle_timer
+
+echo 7102 > /sys/class/devfreq/qcom,gpubw.19/min_freq
+
+echo performance > /sys/class/kgsl/kgsl-3d0/devfreq/governor
+echo 300000000 > /sys/class/kgsl/kgsl-3d0/devfreq/min_freq
+echo 300000000 > /sys/class/kgsl/kgsl-3d0/devfreq/max_freq
+
+echo 4 > /sys/class/kgsl/kgsl-3d0/min_pwrlevel
+echo 4 > /sys/class/kgsl/kgsl-3d0/max_pwrlevel
diff --git a/scripts/perf-setup/dragon-setup.sh b/scripts/perf-setup/dragon-setup.sh
new file mode 100755
index 0000000..7a50a16
--- /dev/null
+++ b/scripts/perf-setup/dragon-setup.sh
@@ -0,0 +1,22 @@
+# performance testing setup script for dragon device
+
+if [[ "`id -u`" -ne "0" ]]; then
+  echo "WARNING: running as non-root, proceeding anyways..."
+fi
+
+# locking CPU frequency
+
+# note: locking cpu0 is sufficent to cover other cores as well
+echo userspace > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
+echo 1530000 > /sys/devices/system/cpu/cpu0/cpufreq/scaling_min_freq
+echo 1530000 > /sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq
+echo 1530000 > /sys/devices/system/cpu/cpu0/cpufreq/scaling_setspeed
+
+# locking GPU frequency
+
+# note: frequency choices can be found in:
+# cat /sys/class/drm/card0/device/pstate
+
+# select 768 MHz
+# 0a: core 768 MHz emc 1600 MHz
+echo 0a > /sys/class/drm/card0/device/pstate
diff --git a/scripts/perf-setup/sailin-setup.sh b/scripts/perf-setup/sailin-setup.sh
new file mode 100644
index 0000000..c538dc3
--- /dev/null
+++ b/scripts/perf-setup/sailin-setup.sh
@@ -0,0 +1,18 @@
+#Setup for newer devices
+
+if [[ "`id -u`" -ne "0" ]]; then
+  echo "WARNING: running as non-root, proceeding anyways..."
+fi
+
+stop thermal-engine
+stop perfd
+
+echo 0 > /sys/devices/system/cpu/cpu0/online
+echo 0 > /sys/devices/system/cpu/cpu1/online
+
+echo performance  > /sys/devices/system/cpu/cpu2/cpufreq/scaling_governor
+echo 2150400 > /sys/devices/system/cpu/cpu2/cpufreq/scaling_max_freq
+
+echo 13763 > /sys/class/devfreq/soc:qcom,gpubw/max_freq
+echo performance > /sys/class/kgsl/kgsl-3d0/devfreq/governor
+echo -n 624000000 > /sys/class/kgsl/kgsl-3d0/devfreq/max_freq
diff --git a/libraries/base-app-helpers/Android.mk b/tests/example/instrumentation/Android.mk
similarity index 70%
copy from libraries/base-app-helpers/Android.mk
copy to tests/example/instrumentation/Android.mk
index 0111a0a..77ad706 100644
--- a/libraries/base-app-helpers/Android.mk
+++ b/tests/example/instrumentation/Android.mk
@@ -1,5 +1,4 @@
-#
-# Copyright (C) 2016 The Android Open Source Project
+#Copyright (C) 2016 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -12,13 +11,18 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-#
+
 LOCAL_PATH := $(call my-dir)
 
 include $(CLEAR_VARS)
-LOCAL_MODULE := base-app-helpers
-LOCAL_STATIC_JAVA_LIBRARIES := dpad-util
-LOCAL_JAVA_LIBRARIES := ub-uiautomator launcher-helper-lib
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
-include $(BUILD_STATIC_JAVA_LIBRARY)
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_MODULE_TAGS := tests
+LOCAL_PACKAGE_NAME := HelloWorldTests
+
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-test
+LOCAL_CERTIFICATE := platform
+
+LOCAL_COMPATIBILITY_SUITE := device-tests
+
+include $(BUILD_PACKAGE)
diff --git a/tests/example/instrumentation/AndroidManifest.xml b/tests/example/instrumentation/AndroidManifest.xml
new file mode 100644
index 0000000..7b5111a
--- /dev/null
+++ b/tests/example/instrumentation/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT 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="android.test.example.helloworld"
+    android:sharedUserId="android.uid.system" >
+
+    <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="21" />
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="android.test.example.helloworld"
+                     android:label="Hello World Test"/>
+
+</manifest>
diff --git a/tests/example/instrumentation/AndroidTest.xml b/tests/example/instrumentation/AndroidTest.xml
new file mode 100644
index 0000000..244d794
--- /dev/null
+++ b/tests/example/instrumentation/AndroidTest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Runs sample instrumentation test.">
+    <target_preparer class="com.android.tradefed.targetprep.TestFilePushSetup" />
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+        <option name="test-file-name" value="HelloWorldTests.apk" />
+    </target_preparer>
+    <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer" />
+    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer" />
+
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-tag" value="SampleInstrumentationTest" />
+    <test class="com.android.tradefed.testtype.InstrumentationTest" >
+        <option name="package" value="android.test.example.helloworld" />
+        <option name="runner" value="android.support.test.runner.AndroidJUnitRunner" />
+    </test>
+</configuration>
diff --git a/tests/example/instrumentation/src/android/test/example/helloworld/HelloWorldTest.java b/tests/example/instrumentation/src/android/test/example/helloworld/HelloWorldTest.java
new file mode 100644
index 0000000..911769e
--- /dev/null
+++ b/tests/example/instrumentation/src/android/test/example/helloworld/HelloWorldTest.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.test.example.helloworld;
+
+import android.support.test.filters.SmallTest;
+import android.util.Log;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class HelloWorldTest {
+
+    private static final String TAG = HelloWorldTest.class.getSimpleName();
+
+    @BeforeClass
+    public static void beforeClass() {
+        Log.d(TAG, "beforeClass()");
+    }
+
+    @AfterClass
+    public static void afterClass() {
+        Log.d(TAG, "afterClass()");
+    }
+
+    @Before
+    public void before() {
+        Log.d(TAG, "before()");
+    }
+
+    @After
+    public void after() {
+        Log.d(TAG, "after()");
+    }
+
+    @Test
+    @SmallTest
+    public void testHelloWorld() {
+        Log.d(TAG, "testHelloWorld()");
+        Assert.assertNotEquals("Hello", "world");
+    }
+
+    @Test
+    @SmallTest
+    public void testHalloWelt() {
+        Log.d(TAG, "testHalloWelt()");
+        Assert.assertNotEquals("Hallo", "Welt");
+    }
+}
diff --git a/libraries/base-app-helpers/Android.mk b/tests/example/native/Android.mk
similarity index 73%
copy from libraries/base-app-helpers/Android.mk
copy to tests/example/native/Android.mk
index 0111a0a..b68d14c 100644
--- a/libraries/base-app-helpers/Android.mk
+++ b/tests/example/native/Android.mk
@@ -1,4 +1,3 @@
-#
 # Copyright (C) 2016 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,13 +11,18 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-#
+
 LOCAL_PATH := $(call my-dir)
 
 include $(CLEAR_VARS)
-LOCAL_MODULE := base-app-helpers
-LOCAL_STATIC_JAVA_LIBRARIES := dpad-util
-LOCAL_JAVA_LIBRARIES := ub-uiautomator launcher-helper-lib
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
-include $(BUILD_STATIC_JAVA_LIBRARY)
+LOCAL_SRC_FILES := \
+  HelloWorldTest.cpp
+
+LOCAL_MODULE := hello_world_test
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_COMPATIBILITY_SUITE := device-tests
+
+include $(BUILD_NATIVE_TEST)
+
diff --git a/tests/example/native/AndroidTest.xml b/tests/example/native/AndroidTest.xml
new file mode 100644
index 0000000..aea2225
--- /dev/null
+++ b/tests/example/native/AndroidTest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Config for APCT native hello world test cases">
+    <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+        <option name="cleanup" value="true" />
+        <option name="push" value="hello_world_test->/data/local/tmp/hello_world_test" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.GTest" >
+        <option name="native-test-device-path" value="/data/local/tmp" />
+        <option name="module-name" value="hello_world_test" />
+        <option name="runtime-hint" value="8m" />
+    </test>
+</configuration>
diff --git a/tests/jank/notificationsgenerator_wear/src/com/google/android/wearable/support/CustomNotificationRemoteInputActivity.java b/tests/example/native/HelloWorldTest.cpp
similarity index 80%
rename from tests/jank/notificationsgenerator_wear/src/com/google/android/wearable/support/CustomNotificationRemoteInputActivity.java
rename to tests/example/native/HelloWorldTest.cpp
index b58f8df..25408d8 100644
--- a/tests/jank/notificationsgenerator_wear/src/com/google/android/wearable/support/CustomNotificationRemoteInputActivity.java
+++ b/tests/example/native/HelloWorldTest.cpp
@@ -1,5 +1,4 @@
 /*
-
  * Copyright (C) 2016 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -15,10 +14,11 @@
  * limitations under the License.
  */
 
-package com.google.android.wearable.support;
+#include <gtest/gtest.h>
 
-import android.app.Activity;
+#include <stdio.h>
 
-public class CustomNotificationRemoteInputActivity extends Activity{
-
+TEST(HelloWorldTest, PrintHelloWorld) {
+    printf("Hello, World!");
 }
+
diff --git a/tests/functional/applinktests/Android.mk b/tests/functional/applinktests/Android.mk
index 8beeae9..0d8ecdb 100644
--- a/tests/functional/applinktests/Android.mk
+++ b/tests/functional/applinktests/Android.mk
@@ -24,9 +24,12 @@
 LOCAL_STATIC_JAVA_LIBRARIES := \
     launcher-helper-lib \
     ub-uiautomator \
+    platform-test-annotations \
     junit
 
 LOCAL_PACKAGE_NAME := AppLinkFunctionalTests
 LOCAL_CERTIFICATE := platform
 
+LOCAL_COMPATIBILITY_SUITE := device-tests
+
 include $(BUILD_PACKAGE)
diff --git a/tests/functional/applinktests/AndroidTest.xml b/tests/functional/applinktests/AndroidTest.xml
new file mode 100644
index 0000000..709e6b8
--- /dev/null
+++ b/tests/functional/applinktests/AndroidTest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Runs AppLink Functional Tests.">
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+        <option name="test-file-name" value="AppLinkFunctionalTests.apk" />
+        <option name="test-file-name" value="AppLinkTestApp.apk" />
+    </target_preparer>
+
+    <option name="test-tag" value="ApplinkTests" />
+    <test class="com.android.tradefed.testtype.InstrumentationTest" >
+        <option name="package" value="com.android.functional.applinktests" />
+        <option name="runner" value="android.test.InstrumentationTestRunner" />
+    </test>
+</configuration>
diff --git a/tests/functional/appsmoke/Android.mk b/tests/functional/appsmoke/Android.mk
index 17d41c9..0fb3c72 100644
--- a/tests/functional/appsmoke/Android.mk
+++ b/tests/functional/appsmoke/Android.mk
@@ -23,4 +23,6 @@
 LOCAL_STATIC_JAVA_LIBRARIES := ub-uiautomator launcher-helper-lib android-support-test
 LOCAL_CERTIFICATE := platform
 
+LOCAL_COMPATIBILITY_SUITE := device-tests
+
 include $(BUILD_PACKAGE)
diff --git a/tests/functional/appsmoke/AndroidTest.xml b/tests/functional/appsmoke/AndroidTest.xml
new file mode 100644
index 0000000..cd0c9a1
--- /dev/null
+++ b/tests/functional/appsmoke/AndroidTest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Runs Prebuilt App Smoke Test.">
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+        <option name="test-file-name" value="AppSmoke.apk" />
+    </target_preparer>
+
+    <option name="test-tag" value="AppSmoke" />
+    <test class="com.android.tradefed.testtype.InstrumentationTest" >
+        <option name="package" value="android.test.appsmoke" />
+        <option name="runner" value="android.support.test.runner.AndroidJUnitRunner" />
+    </test>
+</configuration>
diff --git a/tests/functional/appsmoke/src/android/test/appsmoke/AppSmokeTest.java b/tests/functional/appsmoke/src/android/test/appsmoke/AppSmokeTest.java
index d061304..dcf6197 100644
--- a/tests/functional/appsmoke/src/android/test/appsmoke/AppSmokeTest.java
+++ b/tests/functional/appsmoke/src/android/test/appsmoke/AppSmokeTest.java
@@ -16,13 +16,14 @@
 
 package android.test.appsmoke;
 
-import android.app.ActivityManagerNative;
+import android.app.ActivityManager;
 import android.app.IActivityController;
 import android.app.Instrumentation;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.LauncherActivityInfo;
 import android.content.pm.LauncherApps;
+import android.content.pm.PackageManager;
 import android.os.Bundle;
 import android.os.RemoteException;
 import android.os.SystemClock;
@@ -66,6 +67,7 @@
 
     private boolean mAppHasError = false;
     private boolean mLaunchIntentDetected = false;
+    private boolean mHasLeanback = false;
     private ILauncherStrategy mLauncherStrategy = null;
     private static UiDevice sDevice = null;
 
@@ -202,8 +204,16 @@
 
     @Before
     public void before() throws RemoteException {
-        ActivityManagerNative.getDefault().setActivityController(mActivityController, false);
-        mLauncherStrategy = LauncherStrategyFactory.getInstance(sDevice).getLauncherStrategy();
+        ActivityManager.getService().setActivityController(mActivityController, false);
+        LauncherStrategyFactory factory = LauncherStrategyFactory.getInstance(sDevice);
+        mLauncherStrategy = factory.getLauncherStrategy();
+        // Inject an instance of instrumentation only if leanback. This enables to launch any app
+        // in the Apps and Games row on leanback launcher.
+        Instrumentation instr = InstrumentationRegistry.getInstrumentation();
+        mHasLeanback = hasLeanback(instr.getTargetContext());
+        if (mHasLeanback) {
+            factory.getLeanbackLauncherStrategy().setInstrumentation(instr);
+        }
         mAppHasError = false;
         mLaunchIntentDetected = false;
     }
@@ -217,9 +227,9 @@
     @After
     public void after() throws RemoteException {
         sDevice.pressHome();
-        ActivityManagerNative.getDefault().forceStopPackage(
+        ActivityManager.getService().forceStopPackage(
                 mAppInfo.packageName, UserHandle.USER_ALL);
-        ActivityManagerNative.getDefault().setActivityController(null, false);
+        ActivityManager.getService().setActivityController(null, false);
     }
 
     @AfterClass
@@ -248,12 +258,23 @@
     }
 
     private void pokeApp() {
-        int w = sDevice.getDisplayWidth();
-        int h = sDevice.getDisplayHeight();
-        int dY = h / 4;
-        boolean ret = sDevice.swipe(w / 2, h / 2 + dY, w / 2, h / 2 - dY, 40);
-        if (!ret) {
-            Log.w(TAG, "Failed while attempting to poke front end window with swipe");
+        // The swipe action on leanback launcher that doesn't support swipe gesture may
+        // cause unnecessary focus change and test to fail.
+        // Use the dpad key to poke the app instead.
+        if (!mHasLeanback) {
+            int w = sDevice.getDisplayWidth();
+            int h = sDevice.getDisplayHeight();
+            int dY = h / 4;
+            boolean ret = sDevice.swipe(w / 2, h / 2 + dY, w / 2, h / 2 - dY, 40);
+            if (!ret) {
+                Log.w(TAG, "Failed while attempting to poke front end window with swipe");
+            }
+        } else {
+            sDevice.pressDPadUp();
         }
     }
+
+    private boolean hasLeanback(Context context) {
+        return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK);
+    }
 }
diff --git a/libraries/base-app-helpers/Android.mk b/tests/functional/calculator/Android.mk
similarity index 63%
copy from libraries/base-app-helpers/Android.mk
copy to tests/functional/calculator/Android.mk
index 0111a0a..3dd4073 100644
--- a/libraries/base-app-helpers/Android.mk
+++ b/tests/functional/calculator/Android.mk
@@ -1,5 +1,4 @@
-#
-# Copyright (C) 2016 The Android Open Source Project
+# Copyright (C) 2017 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -12,13 +11,22 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-#
+
 LOCAL_PATH := $(call my-dir)
-
 include $(CLEAR_VARS)
-LOCAL_MODULE := base-app-helpers
-LOCAL_STATIC_JAVA_LIBRARIES := dpad-util
-LOCAL_JAVA_LIBRARIES := ub-uiautomator launcher-helper-lib
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
-include $(BUILD_STATIC_JAVA_LIBRARY)
+LOCAL_PACKAGE_NAME := CalculatorFunctionalTests
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_MODULE_TAGS := tests
+LOCAL_STATIC_JAVA_LIBRARIES := \
+    android-support-test \
+    launcher-helper-lib \
+    metrics-helper-lib \
+    ub-uiautomator \
+    services.core
+
+LOCAL_JAVA_LIBRARIES := legacy-android-test
+
+#LOCAL_SDK_VERSION := current
+
+include $(BUILD_PACKAGE)
diff --git a/tests/functional/calculator/AndroidManifest.xml b/tests/functional/calculator/AndroidManifest.xml
new file mode 100644
index 0000000..0b68441
--- /dev/null
+++ b/tests/functional/calculator/AndroidManifest.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT 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.android.calculator.functional" >
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <uses-sdk
+        android:minSdkVersion="21"
+        android:targetSdkVersion="24" />
+
+    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+    <uses-permission android:name="android.permission.READ_LOGS" />
+
+    <instrumentation
+        android:name="android.support.test.runner.AndroidJUnitRunner"
+        android:label="Android Calculator Functional Tests"
+        android:targetPackage="com.android.calculator.functional" />
+
+</manifest>
diff --git a/tests/functional/calculator/src/com/android/calculator/functional/CalculatorHelper.java b/tests/functional/calculator/src/com/android/calculator/functional/CalculatorHelper.java
new file mode 100644
index 0000000..4b45193
--- /dev/null
+++ b/tests/functional/calculator/src/com/android/calculator/functional/CalculatorHelper.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.calculator.functional;
+
+import android.content.Context;
+import android.content.Intent;
+import android.support.test.launcherhelper.ILauncherStrategy;
+import android.support.test.launcherhelper.LauncherStrategyFactory;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.UiObject;
+import android.support.test.uiautomator.Until;
+import android.support.test.uiautomator.UiSelector;
+import android.test.InstrumentationTestCase;
+import android.util.Log;
+import junit.framework.Assert;
+
+public class CalculatorHelper {
+    private static CalculatorHelper mInstance = null;
+    private static final int SHORT_TIMEOUT = 1000;
+    private static final int LONG_TIMEOUT = 2000;
+    public static final String PACKAGE_NAME = "com.google.android.calculator";
+    public static final String APP_NAME = "Calculator";
+    public static final String TEST_TAG = "CalculatorTests";
+    public final int TIMEOUT = 500;
+    private Context mContext = null;
+    private UiDevice mDevice = null;
+    public ILauncherStrategy mLauncherStrategy;
+
+    private CalculatorHelper(UiDevice device, Context context) {
+       mDevice = device;
+       mContext = context;
+       mLauncherStrategy = LauncherStrategyFactory.getInstance(mDevice).getLauncherStrategy();
+    }
+
+    public static CalculatorHelper getInstance(UiDevice device, Context context) {
+        if (mInstance == null) {
+        mInstance = new CalculatorHelper(device, context);
+      }
+        return mInstance;
+    }
+
+    public void launchApp(String packageName, String appName) {
+        if (!mDevice.hasObject(By.pkg(packageName).depth(0))) {
+        mLauncherStrategy.launch(appName, packageName);
+      }
+    }
+
+    public void clickButton(String resource_id) {
+        UiObject2 button = mDevice.wait(
+            Until.findObject(By.res(PACKAGE_NAME, resource_id)),
+                SHORT_TIMEOUT);
+        Assert.assertNotNull("Element not found or pressed", button);
+        button.click();
+    }
+
+    public void performCalculation(String input1, String operator, String input2) {
+        clickButton(input1);
+        clickButton(operator);
+        clickButton(input2);
+        clickButton("eq");
+    }
+
+    public void pressLongDigits() {
+      for (int i=1; i<10; i++)    clickButton("digit_"+i);
+    }
+
+    public void pressNumber100000() {
+        clickButton("digit_1");
+        for (int i=0; i<5; i++)   clickButton("digit_0");
+    }
+
+    public String getResultText(String result) {
+        UiObject2 resultText = mDevice.wait(
+            Until.findObject(By.res(PACKAGE_NAME, result)),
+                SHORT_TIMEOUT);
+        Assert.assertNotNull("Result text box not found", resultText);
+        return resultText.getText();
+    }
+
+    public void clearResults(String result) {
+        UiObject2 resultText = mDevice.wait(
+              Until.findObject(By.res(PACKAGE_NAME, result)),
+                  SHORT_TIMEOUT);
+        Assert.assertNotNull("Result box not found", resultText);
+        resultText.clear();
+    }
+
+    public void scrollResults(String result) {
+        UiObject2 resultText = mDevice.wait(
+            Until.findObject(By.res(PACKAGE_NAME, result)),
+                  SHORT_TIMEOUT);
+        Assert.assertNotNull("Result text box not found", resultText);
+        resultText.swipe(Direction.LEFT, 1.0f, 5000);
+        Assert.assertEquals("Scroll failed","…41578750190521", getResultText("result"));
+        mDevice.waitForIdle();
+        resultText.swipe(Direction.RIGHT, 1.0f, 5000);
+    }
+
+    public void showAdvancedPad(){
+        UiObject2 padAdvanced = mDevice.wait(
+              Until.findObject(By.res(PACKAGE_NAME, "pad_advanced")),
+                  SHORT_TIMEOUT);
+            if (padAdvanced.isClickable()) {//don't click if already pad opened
+                padAdvanced.click();
+                Assert.assertNotNull("Advanced pad not found", padAdvanced);
+            }
+        mDevice.waitForIdle();
+    }
+
+    public void dismissAdvancedPad() {
+        UiObject2 padAdvanced = mDevice.wait(
+              Until.findObject(By.res(PACKAGE_NAME, "pad_advanced")),
+                  SHORT_TIMEOUT);
+        padAdvanced.swipe(Direction.RIGHT, 1.0f);
+        mDevice.waitForIdle();
+    }
+}
diff --git a/tests/functional/calculator/src/com/android/calculator/functional/CalculatorTests.java b/tests/functional/calculator/src/com/android/calculator/functional/CalculatorTests.java
new file mode 100644
index 0000000..183c07a
--- /dev/null
+++ b/tests/functional/calculator/src/com/android/calculator/functional/CalculatorTests.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.calculator.functional;
+
+import android.content.Context;
+import android.content.Intent;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.view.inputmethod.InputMethodManager;
+
+public class CalculatorTests extends InstrumentationTestCase {
+    private CalculatorHelper mCalculatorHelper = null;
+    private static final int SHORT_TIMEOUT = 1000;
+    private static final int LONG_TIMEOUT = 2000;
+    private UiDevice mDevice = null;
+    private Context mContext;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mDevice = UiDevice.getInstance(getInstrumentation());
+        mContext = getInstrumentation().getContext();
+        mDevice.setOrientationNatural();
+        mDevice.pressHome();
+        mCalculatorHelper = CalculatorHelper.getInstance(mDevice, mContext);
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        mDevice.pressHome();
+        mDevice.unfreezeRotation();
+        super.tearDown();
+    }
+
+    // Launch the app
+    public void launchCalculator() {
+        mCalculatorHelper.launchApp(CalculatorHelper.PACKAGE_NAME, CalculatorHelper.APP_NAME);
+        mDevice.waitForIdle();
+    }
+
+    @SmallTest
+    //Test to verify basic addition functionality
+    public void testAdd() throws Exception {
+        launchCalculator();
+        mCalculatorHelper.performCalculation("digit_9","op_add","digit_9");
+        assertEquals("Results are wrong", "18", mCalculatorHelper.getResultText("result"));
+        mDevice.waitForIdle();
+        mDevice.pressBack();
+    }
+
+    @SmallTest
+    //Test to verify basic subraction functionality
+    public void testSubtract() throws Exception {
+        launchCalculator();
+        mCalculatorHelper.performCalculation("digit_6","op_sub","digit_4");
+        assertEquals("Results are wrong","2", mCalculatorHelper.getResultText("result"));
+        mDevice.waitForIdle();
+        mDevice.pressBack();
+    }
+
+    @SmallTest
+    //Test to verify basic multiplication functionality
+    public void testMultiply() throws Exception {
+        launchCalculator();
+        mCalculatorHelper.performCalculation("digit_7","op_mul","digit_5");
+        assertEquals("Results are wrong","35", mCalculatorHelper.getResultText("result"));
+        mDevice.waitForIdle();
+        mDevice.pressBack();
+    }
+
+    @SmallTest
+    //Test to verify basic divition functionality
+    public void testDivide() throws Exception {
+        launchCalculator();
+        mCalculatorHelper.performCalculation("digit_8","op_div","digit_2");
+        assertEquals("Results are wrong","4", mCalculatorHelper.getResultText("result"));
+        mDevice.waitForIdle();
+        mDevice.pressBack();
+    }
+
+    @SmallTest
+    //Test to verify to clear the results
+    public void testClearButton() throws Exception {
+        launchCalculator();
+        mCalculatorHelper.performCalculation("digit_9","op_mul","digit_9");
+        mCalculatorHelper.clickButton("clr");
+        UiObject2 deleteButton = mDevice.wait(
+            Until.findObject(By.res(mCalculatorHelper.PACKAGE_NAME, "del")),
+                SHORT_TIMEOUT);
+        if (deleteButton !=null) { //Verify the button is changed to delete after clear
+            assertNull(mCalculatorHelper.getResultText("result"));
+        }
+        mDevice.waitForIdle();
+        mDevice.pressBack();
+    }
+
+    @SmallTest
+    // Test divide by zero error
+    public void testDivideByZero() throws Exception {
+        launchCalculator();
+        mCalculatorHelper.performCalculation("digit_1", "op_div","digit_0");
+        assertEquals("Error", "Can't divide by 0", mCalculatorHelper.getResultText("result"));
+        mDevice.waitForIdle();
+        mDevice.pressBack();
+    }
+
+    @MediumTest
+    // Test Scroll funtion in long results
+    public void testScrollLongResult() throws Exception {
+        launchCalculator();
+        mCalculatorHelper.pressLongDigits();
+        mCalculatorHelper.clickButton("op_mul");
+        mCalculatorHelper.pressLongDigits();
+        mCalculatorHelper.clickButton("eq");
+        mCalculatorHelper.scrollResults("result");
+        assertEquals("Scroll failed","1.52415787501E16", mCalculatorHelper.getResultText("result"));
+        mDevice.waitForIdle();
+        mDevice.pressBack();
+    }
+
+    @MediumTest
+    // Test to verify the advanced panel and basic operation works
+    public void testAdvancedOperation() throws Exception {
+        launchCalculator();
+        mCalculatorHelper.showAdvancedPad();
+        mCalculatorHelper.clickButton("fun_cos");
+        mCalculatorHelper.dismissAdvancedPad();
+        mCalculatorHelper.clickButton("digit_0");
+        mCalculatorHelper.clickButton("eq");
+        assertEquals("Results are wrong", "1",mCalculatorHelper.getResultText("result"));
+        mDevice.waitForIdle();
+        mDevice.pressBack();
+    }
+
+    @MediumTest
+    //Test timeouts on complex calculations
+    public void testComplexCalculationTimeout() throws Exception {
+        launchCalculator();
+        mCalculatorHelper.pressNumber100000();
+        mCalculatorHelper.showAdvancedPad();
+        mCalculatorHelper.clickButton("op_fact");
+        mCalculatorHelper.dismissAdvancedPad();
+        mCalculatorHelper.clickButton("eq");
+        mDevice.waitForIdle();
+        UiObject2 alertTitle = mDevice.wait(
+            Until.findObject(By.res("android:id/alertTitle")), SHORT_TIMEOUT);
+        assertNotNull("Alert pop up not found", alertTitle);
+        UiObject2 msgText = mDevice.wait(
+            Until.findObject(By.res(mCalculatorHelper.PACKAGE_NAME, "message")),
+                SHORT_TIMEOUT);
+        mDevice.waitForIdle();
+        assertEquals("Message not found", "Value may be infinite or undefined.",msgText.getText());
+        UiObject2 dismissButton = mDevice.wait(
+            Until.findObject(By.res("android:id/button2")), SHORT_TIMEOUT);
+        assertNotNull("Dismiss button not found", dismissButton);
+        dismissButton.click();
+        mCalculatorHelper.clearResults("formula");
+        mDevice.pressBack();
+    }
+
+    @MediumTest
+    // Test DEG/RAD switch happens and display changes
+    public void testDegRadSwitch() throws Exception {
+        launchCalculator();
+        mCalculatorHelper.showAdvancedPad();
+
+        UiObject2 toggleButton = mDevice.wait(
+            Until.findObject(By.res(mCalculatorHelper.PACKAGE_NAME, "toggle_mode")),
+                SHORT_TIMEOUT);
+        assertNotNull("Toggle Button not found", toggleButton);
+
+        UiObject2 modeBox = mDevice.wait(
+            Until.findObject(By.res(mCalculatorHelper.PACKAGE_NAME, "mode")),
+                SHORT_TIMEOUT);
+        assertNotNull("Mode Box not found", modeBox);
+
+        for (int i=0; i<3; i++) { //Test the toggle button 3 times
+            mCalculatorHelper.clickButton("toggle_mode");
+            mDevice.waitForIdle();
+            assertNotSame("Switch Failed",toggleButton.getText(),modeBox.getText());
+        }
+        mCalculatorHelper.dismissAdvancedPad();
+        mDevice.waitForIdle();
+        mDevice.pressBack();
+    }
+}
diff --git a/libraries/base-app-helpers/Android.mk b/tests/functional/devicehealthtests/Android.mk
similarity index 60%
copy from libraries/base-app-helpers/Android.mk
copy to tests/functional/devicehealthtests/Android.mk
index 0111a0a..e35b301 100644
--- a/libraries/base-app-helpers/Android.mk
+++ b/tests/functional/devicehealthtests/Android.mk
@@ -1,24 +1,29 @@
-#
-# Copyright (C) 2016 The Android Open Source Project
+# Copyright 2016 Google Inc. All Rights Reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
 # You may obtain a copy of the License at
 #
-#      http://www.apache.org/licenses/LICENSE-2.0
+#     http://www.apache.org/licenses/LICENSE-2.0
 #
 # Unless required by applicable law or agreed to in writing, software
 # distributed under the License is distributed on an "AS IS" BASIS,
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-#
-LOCAL_PATH := $(call my-dir)
 
+LOCAL_PATH := $(call my-dir)
 include $(CLEAR_VARS)
-LOCAL_MODULE := base-app-helpers
-LOCAL_STATIC_JAVA_LIBRARIES := dpad-util
-LOCAL_JAVA_LIBRARIES := ub-uiautomator launcher-helper-lib
+
+LOCAL_MODULE_TAGS := tests
+
+# Only compile source java files in this apk.
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
-include $(BUILD_STATIC_JAVA_LIBRARY)
+LOCAL_PACKAGE_NAME := DeviceHealthTests
+LOCAL_CERTIFICATE := platform
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-test platform-test-annotations
+
+LOCAL_COMPATIBILITY_SUITE := device-tests
+
+include $(BUILD_PACKAGE)
diff --git a/tests/functional/devicehealthtests/AndroidManifest.xml b/tests/functional/devicehealthtests/AndroidManifest.xml
new file mode 100644
index 0000000..e2beb7f
--- /dev/null
+++ b/tests/functional/devicehealthtests/AndroidManifest.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT 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.android.devicehealth.tests"
+    android:sharedUserId="com.android.devicehealth.tests"
+    android:versionCode="1"
+    android:versionName="1.0" >
+
+   <uses-permission android:name="android.permission.READ_LOGS" />
+
+   <uses-sdk
+        android:minSdkVersion="22"
+        android:targetSdkVersion="25" />
+
+    <instrumentation
+        android:name="android.support.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.devicehealth.tests" />
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+</manifest>
diff --git a/tests/functional/devicehealthtests/AndroidTest.xml b/tests/functional/devicehealthtests/AndroidTest.xml
new file mode 100644
index 0000000..4febb7a
--- /dev/null
+++ b/tests/functional/devicehealthtests/AndroidTest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Runs DeviceHealthTests.">
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+        <option name="test-file-name" value="DeviceHealthTests.apk" />
+    </target_preparer>
+
+    <option name="test-tag" value="DeviceHealthTests" />
+    <test class="com.android.tradefed.testtype.InstrumentationTest" >
+        <option name="package" value="com.android.devicehealth.tests" />
+        <option name="runner" value="android.support.test.runner.AndroidJUnitRunner" />
+    </test>
+</configuration>
diff --git a/tests/functional/devicehealthtests/src/com/android/devicehealth/tests/BasicHealthCheck.java b/tests/functional/devicehealthtests/src/com/android/devicehealth/tests/BasicHealthCheck.java
new file mode 100644
index 0000000..5bf1125
--- /dev/null
+++ b/tests/functional/devicehealthtests/src/com/android/devicehealth/tests/BasicHealthCheck.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.devicehealth.tests;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+import android.platform.test.annotations.GlobalPresubmit;
+
+/**
+ * Tests used for basic device health validation after the device boot is completed. This test class
+ * can be used to add more tests in the future for additional basic device health validation after
+ * the device boot is completed. This test is used for global presubmit, any dropbox label checked
+ * showing failures must be resolved immediately, or have flaky ones moved into
+ * {@link BasicHealthCheckPostSubmit} instead.
+ */
+@GlobalPresubmit
+@RunWith(Parameterized.class)
+public class BasicHealthCheck extends HealthCheckBase {
+
+    @Parameter
+    public String mDropboxLabel;
+
+    @Parameters(name = "{0}")
+    public static String[] dropboxLabels() {
+        return new String[] {
+                "system_server_crash",
+                "system_server_native_crash",
+                "system_server_anr",
+                "system_app_crash",
+                };
+    }
+
+    /**
+     * Test if there are app crashes in the device by checking system_app_crash,
+     * system_app_native_crash or system_server_anr using DropBoxManager service.
+     */
+    @Test
+    public void checkCrash() {
+        checkCrash(mDropboxLabel);
+    }
+
+}
diff --git a/tests/functional/devicehealthtests/src/com/android/devicehealth/tests/BasicHealthCheckPostSubmit.java b/tests/functional/devicehealthtests/src/com/android/devicehealth/tests/BasicHealthCheckPostSubmit.java
new file mode 100644
index 0000000..41161ea
--- /dev/null
+++ b/tests/functional/devicehealthtests/src/com/android/devicehealth/tests/BasicHealthCheckPostSubmit.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.devicehealth.tests;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+/**
+ * Tests used for basic device health validation (ex: system app crash or system app
+ * native crash) after the device boot is completed. This test class can be used to
+ * add more tests in the future for additional basic device health validation
+ * after the device boot is completed.
+ */
+@RunWith(Parameterized.class)
+public class BasicHealthCheckPostSubmit extends HealthCheckBase {
+
+    @Parameter
+    public String mDropboxLabel;
+
+    @Parameters(name = "{0}")
+    public static String[] dropboxLabels() {
+        return new String[] {
+                "system_app_anr", // b/35626956
+                "SYSTEM_TOMBSTONE", // b/36066697
+                "system_app_native_crash", // b/38454327
+                };
+    }
+
+    /**
+     * Test if there are app crashes in the device by checking system_app_crash,
+     * system_app_native_crash or system_server_anr using DropBoxManager service.
+     */
+    @Test
+    public void checkCrash() {
+        checkCrash(mDropboxLabel);
+    }
+
+}
diff --git a/tests/functional/devicehealthtests/src/com/android/devicehealth/tests/HealthCheckBase.java b/tests/functional/devicehealthtests/src/com/android/devicehealth/tests/HealthCheckBase.java
new file mode 100644
index 0000000..6341b74
--- /dev/null
+++ b/tests/functional/devicehealthtests/src/com/android/devicehealth/tests/HealthCheckBase.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.devicehealth.tests;
+
+import org.junit.Assert;
+import org.junit.Before;
+
+import android.content.Context;
+import android.os.DropBoxManager;
+import android.support.test.InstrumentationRegistry;
+
+abstract class HealthCheckBase {
+
+    private Context mContext;
+
+    @Before
+    public void setUp() throws Exception {
+        mContext = InstrumentationRegistry.getInstrumentation().getContext();
+    }
+
+    /**
+     * Check dropbox service for a particular label and assert if found
+     */
+    protected void checkCrash(String label) {
+        DropBoxManager dropbox = (DropBoxManager) mContext
+                .getSystemService(Context.DROPBOX_SERVICE);
+        Assert.assertNotNull("Unable access the DropBoxManager service", dropbox);
+
+        long timestamp = 0;
+        DropBoxManager.Entry entry = null;
+        int crashCount = 0;
+        StringBuilder errorDetails = new StringBuilder("Error details:\n");
+        while (null != (entry = dropbox.getNextEntry(label, timestamp))) {
+            try {
+                crashCount++;
+                errorDetails.append(label);
+                errorDetails.append(": ");
+                errorDetails.append(entry.getText(70));
+                errorDetails.append("    ...\n");
+            } finally {
+                entry.close();
+            }
+            timestamp = entry.getTimeMillis();
+        }
+        Assert.assertEquals(errorDetails.toString(), 0, crashCount);
+    }
+}
diff --git a/tests/functional/devicehealthtests/src/com/android/devicehealth/tests/SensorsBootCheck.java b/tests/functional/devicehealthtests/src/com/android/devicehealth/tests/SensorsBootCheck.java
new file mode 100644
index 0000000..c2d0553
--- /dev/null
+++ b/tests/functional/devicehealthtests/src/com/android/devicehealth/tests/SensorsBootCheck.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.devicehealth.tests;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.hardware.Sensor;
+import android.hardware.SensorManager;
+import android.platform.test.annotations.GlobalPresubmit;
+import android.support.test.InstrumentationRegistry;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.runner.RunWith;
+import org.junit.Test;
+
+/*
+ * Tests used for basic sensors validation after the device boot is completed.
+ * This test is used for global presubmit.
+ */
+
+@GlobalPresubmit
+public class SensorsBootCheck {
+    private Context mContext;
+    private PackageManager mPackageManager;
+    private SensorManager mSensorManager;
+
+    @Before
+    public void setUp() throws Exception {
+        mContext = InstrumentationRegistry.getInstrumentation().getContext();
+    }
+
+    /*
+     * Test if sensors are available on the device as advertised.
+     */
+    @Test
+    public void checkSensors() {
+        /*
+         * This is a simple test that checks if sensors that are expected on the device are actually
+         * available. This is a generic test and relies on the fact that a device must declare some
+         * of the sensors it supports as features. We do not check the actual function of the
+         * sensors in this method.
+         */
+
+        int numErrors = 0;
+
+        mPackageManager = mContext.getPackageManager();
+        Assert.assertNotNull("Package Manager not found", mPackageManager);
+
+        mSensorManager = (SensorManager) mContext.getSystemService(Context.SENSOR_SERVICE);
+        Assert.assertNotNull("Sensor Manager not found", mSensorManager);
+
+        StringBuilder errorDetails = new StringBuilder("Error details: \n");
+        numErrors += isSensorMissing(PackageManager.FEATURE_SENSOR_ACCELEROMETER,
+            Sensor.TYPE_ACCELEROMETER, errorDetails) ? 1 : 0;
+        numErrors += isSensorMissing(PackageManager.FEATURE_SENSOR_AMBIENT_TEMPERATURE,
+            Sensor.TYPE_AMBIENT_TEMPERATURE, errorDetails) ? 1 : 0;
+        numErrors += isSensorMissing(PackageManager.FEATURE_SENSOR_BAROMETER,
+            Sensor.TYPE_PRESSURE, errorDetails) ? 1 : 0;
+        numErrors += isSensorMissing(PackageManager.FEATURE_SENSOR_COMPASS,
+            Sensor.TYPE_MAGNETIC_FIELD, errorDetails) ? 1 : 0;
+        numErrors += isSensorMissing(PackageManager.FEATURE_SENSOR_GYROSCOPE,
+            Sensor.TYPE_GYROSCOPE, errorDetails) ? 1 : 0;
+        numErrors += isSensorMissing(PackageManager.FEATURE_SENSOR_LIGHT,
+            Sensor.TYPE_LIGHT, errorDetails) ? 1 : 0;
+        numErrors += isSensorMissing(PackageManager.FEATURE_SENSOR_PROXIMITY,
+            Sensor.TYPE_PROXIMITY, errorDetails) ? 1 : 0;
+        numErrors += isSensorMissing(PackageManager.FEATURE_SENSOR_RELATIVE_HUMIDITY,
+            Sensor.TYPE_RELATIVE_HUMIDITY, errorDetails) ? 1 : 0;
+        numErrors += isSensorMissing(PackageManager.FEATURE_SENSOR_STEP_COUNTER,
+            Sensor.TYPE_STEP_COUNTER, errorDetails) ? 1 : 0;
+        numErrors += isSensorMissing(PackageManager.FEATURE_SENSOR_STEP_DETECTOR,
+            Sensor.TYPE_STEP_DETECTOR, errorDetails) ? 1 : 0;
+
+        // TODO: test heart rate and other related sensor types.
+
+        Assert.assertEquals(errorDetails.toString(), numErrors, 0);
+    }
+
+    private boolean isSensorMissing(String featureString, int sensorType, StringBuilder errString) {
+        if (!mPackageManager.hasSystemFeature(featureString)) {
+            // no claim to support the sensor, do not check farther
+            return false;
+        }
+
+        if (mSensorManager.getDefaultSensor(sensorType) == null) {
+            errString.append("Cannot find sensor type " + sensorType + " even though " +
+                featureString + " is defined\n");
+            return true;
+        }
+        return false;
+    }
+}
diff --git a/tests/functional/downloadapp/Android.mk b/tests/functional/downloadapp/Android.mk
index f833979..a2c5464 100644
--- a/tests/functional/downloadapp/Android.mk
+++ b/tests/functional/downloadapp/Android.mk
@@ -15,4 +15,6 @@
 LOCAL_PACKAGE_NAME := DownloadAppFunctionalTests
 LOCAL_CERTIFICATE := platform
 
+LOCAL_COMPATIBILITY_SUITE := device-tests
+
 include $(BUILD_PACKAGE)
diff --git a/tests/functional/downloadapp/AndroidTest.xml b/tests/functional/downloadapp/AndroidTest.xml
new file mode 100644
index 0000000..e478177
--- /dev/null
+++ b/tests/functional/downloadapp/AndroidTest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Runs DownloadApp Functional Tests.">
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+        <option name="test-file-name" value="DownloadAppFunctionalTests.apk" />
+    </target_preparer>
+
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-tag" value="DownloadAppFunctionalTests" />
+    <test class="com.android.tradefed.testtype.InstrumentationTest" >
+        <option name="package" value="com.android.functional.downloadapp" />
+        <option name="runner" value="android.support.test.runner.AndroidJUnitRunner" />
+    </test>
+</configuration>
diff --git a/tests/functional/notificationtests/Android.mk b/tests/functional/notificationtests/Android.mk
index 97b92fb..a48bf81 100644
--- a/tests/functional/notificationtests/Android.mk
+++ b/tests/functional/notificationtests/Android.mk
@@ -21,6 +21,7 @@
 LOCAL_STATIC_JAVA_LIBRARIES := \
     android-support-test \
     launcher-helper-lib \
+    metrics-helper-lib \
     ub-uiautomator \
     services.core
 
@@ -28,4 +29,6 @@
 
 #LOCAL_SDK_VERSION := current
 
+LOCAL_COMPATIBILITY_SUITE := device-tests
+
 include $(BUILD_PACKAGE)
diff --git a/tests/functional/notificationtests/AndroidManifest.xml b/tests/functional/notificationtests/AndroidManifest.xml
index 970ac39..2fc9b2a 100644
--- a/tests/functional/notificationtests/AndroidManifest.xml
+++ b/tests/functional/notificationtests/AndroidManifest.xml
@@ -28,6 +28,7 @@
     <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
     <uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
     <uses-permission android:name="com.android.alarm.permission.SET_ALARM" />
+    <uses-permission android:name="android.permission.READ_LOGS" />
 
     <instrumentation
         android:name="android.support.test.runner.AndroidJUnitRunner"
diff --git a/tests/functional/notificationtests/AndroidTest.xml b/tests/functional/notificationtests/AndroidTest.xml
new file mode 100644
index 0000000..9e85023
--- /dev/null
+++ b/tests/functional/notificationtests/AndroidTest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Runs Android Notifications Functional Tests.">
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+        <option name="test-file-name" value="NotificationFunctionalTests.apk" />
+    </target_preparer>
+
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-tag" value="android_systemui" />
+    <test class="com.android.tradefed.testtype.InstrumentationTest" >
+        <option name="package" value="com.android.notification.functional" />
+        <option name="runner" value="android.support.test.runner.AndroidJUnitRunner" />
+    </test>
+</configuration>
diff --git a/tests/functional/notificationtests/src/com/android/notification/functional/NotificationBundlingTests.java b/tests/functional/notificationtests/src/com/android/notification/functional/NotificationBundlingTests.java
index 84e3f57..1524646 100644
--- a/tests/functional/notificationtests/src/com/android/notification/functional/NotificationBundlingTests.java
+++ b/tests/functional/notificationtests/src/com/android/notification/functional/NotificationBundlingTests.java
@@ -34,8 +34,8 @@
     private static final int SHORT_TIMEOUT = 200;
     private static final int LONG_TIMEOUT = 2000;
     private static final int GROUP_NOTIFICATION_ID = 1;
-    private static final int CHILD_NOTIFICATION_ID = 100;
-    private static final int SECOND_CHILD_NOTIFICATION_ID = 101;
+    private static final int CHILD_NOTIFICATION_ID = 500;
+    private static final int SECOND_CHILD_NOTIFICATION_ID = 501;
     private static final String BUNDLE_GROUP_KEY = "group_key ";
     private NotificationManager mNotificationManager;
     private UiDevice mDevice = null;
diff --git a/tests/functional/notificationtests/src/com/android/notification/functional/NotificationDNDTests.java b/tests/functional/notificationtests/src/com/android/notification/functional/NotificationDNDTests.java
index 16fd608..994fce4 100644
--- a/tests/functional/notificationtests/src/com/android/notification/functional/NotificationDNDTests.java
+++ b/tests/functional/notificationtests/src/com/android/notification/functional/NotificationDNDTests.java
@@ -2,6 +2,7 @@
 package com.android.notification.functional;
 
 import android.app.Notification;
+import android.app.NotificationChannel;
 import android.app.NotificationManager;
 import android.content.ContentResolver;
 import android.content.Context;
@@ -99,7 +100,7 @@
             mHelper.sendNotificationsWithInLineReply(NOTIFICATION_ID, true);
             Thread.sleep(LONG_TIMEOUT);
             NotificationRecord nr = new NotificationRecord(mContext,
-                    mHelper.getStatusBarNotification(NOTIFICATION_ID));
+                    mHelper.getStatusBarNotification(NOTIFICATION_ID), mHelper.getDefaultChannel());
             ZenModeConfig mConfig = mZenHelper.getConfig();
             ZenModeFiltering zF = new ZenModeFiltering(mContext);
             assertTrue(zF.shouldIntercept(mNotificationManager.getZenMode(), mConfig, nr));
@@ -120,19 +121,23 @@
         mNotificationManager
                 .setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY);
         try {
-            mHelper.showInstalledAppDetails(mContext, "com.android.notification.functional");
+            mHelper.showAppNotificationSettings(mContext);
+            mDevice.wait(Until.findObject(By.textContains("Miscellaneous")), LONG_TIMEOUT)
+                    .click();
             mDevice.wait(Until.findObject(By.textContains("Override Do Not Disturb")), LONG_TIMEOUT)
                     .click();
             Thread.sleep(LONG_TIMEOUT);
             mHelper.sendNotificationsWithInLineReply(NOTIFICATION_ID, true);
             Thread.sleep(LONG_TIMEOUT);
             NotificationRecord nr = new NotificationRecord(mContext,
-                    mHelper.getStatusBarNotification(NOTIFICATION_ID));
+                    mHelper.getStatusBarNotification(NOTIFICATION_ID), mHelper.getDefaultChannel());
             ZenModeConfig mConfig = mZenHelper.getConfig();
             ZenModeFiltering zF = new ZenModeFiltering(mContext);
             assertFalse(zF.shouldIntercept(mZenHelper.getZenMode(), mConfig, nr));
         } finally {
-            mHelper.showInstalledAppDetails(mContext, "com.android.notification.functional");
+            mHelper.showAppNotificationSettings(mContext);
+            mDevice.wait(Until.findObject(By.textContains("Miscellaneous")), LONG_TIMEOUT)
+                    .click();
             mDevice.wait(Until.findObject(By.textContains("Override Do Not Disturb")), LONG_TIMEOUT)
                     .click();
             mNotificationManager.setInterruptionFilter(setting);
@@ -145,16 +150,16 @@
     @LargeTest
     public void testBlockNotification() throws Exception {
         try {
-            mHelper.showInstalledAppDetails(mContext, "com.android.notification.functional");
+            mHelper.showAppNotificationSettings(mContext);
             mDevice.wait(Until.findObject(By.textContains("Block all")), LONG_TIMEOUT).click();
             Thread.sleep(LONG_TIMEOUT);
             mHelper.sendNotificationsWithInLineReply(NOTIFICATION_ID, true);
             Thread.sleep(LONG_TIMEOUT);
             if (mHelper.checkNotificationExistence(NOTIFICATION_ID, true)) {
-                fail(String.format("Notification %s has not benn blocked", NOTIFICATION_ID));
+                fail(String.format("Notification %s has not been blocked", NOTIFICATION_ID));
             }
         } finally {
-            mHelper.showInstalledAppDetails(mContext, "com.android.notification.functional");
+            mHelper.showAppNotificationSettings(mContext);
             mDevice.wait(Until.findObject(By.textContains("Block all")), LONG_TIMEOUT).click();
         }
     }
diff --git a/tests/functional/notificationtests/src/com/android/notification/functional/NotificationHelper.java b/tests/functional/notificationtests/src/com/android/notification/functional/NotificationHelper.java
index 7e36fa8..dc9aeb4 100644
--- a/tests/functional/notificationtests/src/com/android/notification/functional/NotificationHelper.java
+++ b/tests/functional/notificationtests/src/com/android/notification/functional/NotificationHelper.java
@@ -16,27 +16,23 @@
 
 package com.android.notification.functional;
 
-import android.app.AlarmManager;
 import android.app.Instrumentation;
 import android.app.IntentService;
 import android.app.KeyguardManager;
 import android.app.Notification;
-import android.app.Notification.Builder;
+import android.app.NotificationChannel;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.app.RemoteInput;
-import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.Typeface;
 import android.net.Uri;
 import android.os.Handler;
 import android.os.RemoteException;
-import android.os.SystemClock;
 import android.provider.Settings;
 import android.service.notification.StatusBarNotification;
 import android.support.test.uiautomator.By;
-import android.support.test.uiautomator.Direction;
 import android.support.test.uiautomator.UiDevice;
 import android.support.test.uiautomator.UiObject;
 import android.support.test.uiautomator.UiObjectNotFoundException;
@@ -57,14 +53,18 @@
 import java.util.List;
 import java.util.Map;
 
+
 public class NotificationHelper {
 
     private static final String LOG_TAG = NotificationHelper.class.getSimpleName();
-    private static final int LONG_TIMEOUT = 2000;
+    private static final int LONG_TIMEOUT = 2500;
     private static final int SHORT_TIMEOUT = 200;
     private static final String KEY_QUICK_REPLY_TEXT = "quick_reply";
     private static final UiSelector LIST_VIEW = new UiSelector().className(ListView.class);
     private static final UiSelector LIST_ITEM_VALUE = new UiSelector().className(TextView.class);
+    public static final String FIRST_ACTION = "FIRST ACTION";
+    public static final String SECOND_ACTION = "SECOND ACTION";
+    public static final String CONTENT_TITLE = "THIS IS A NOTIFICATION";
 
     private UiDevice mDevice;
     private Instrumentation mInst;
@@ -107,7 +107,11 @@
                 mDevice.pressEnter();
             }
             new UiObject(new UiSelector().text("PIN")).click();
-            clickText("No thanks");
+            // If there's an option to set 'require PIN to start device'
+            // choose 'No thanks', otherwise just skip ahead.
+            if (new UiObject(new UiSelector().text("No thanks")).exists()) {
+                clickText("No thanks");
+            }
             UiObject pinField = new UiObject(new UiSelector().className(EditText.class.getName()));
             pinField.setText(String.format("%04d", pin));
             mDevice.pressEnter();
@@ -164,32 +168,52 @@
     }
 
     public void sendNotification(int id, int visibility, String title) throws Exception {
+        sendNotification(id, visibility, title, false);
+    }
+
+    public void sendNotification(int id, int visibility, String title, boolean buzz)
+            throws Exception {
         Log.v(LOG_TAG, "Sending out notification...");
+        PendingIntent emptyIntent = PendingIntent.getBroadcast(mContext, 0,
+                new Intent("an.action.that.nobody.will.be.listening.for"), 0);
         Intent intent = new Intent(Intent.ACTION_VIEW);
         PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
         CharSequence subtitle = String.valueOf(System.currentTimeMillis());
-        Notification notification = new Notification.Builder(mContext)
+        Notification.Builder notification = new Notification.Builder(mContext)
                 .setSmallIcon(R.drawable.stat_notify_email)
-                .setWhen(System.currentTimeMillis()).setContentTitle(title).setContentText(subtitle)
-                .setContentIntent(pendingIntent).setVisibility(visibility)
+                .setWhen(System.currentTimeMillis())
+                .setContentTitle(title)
+                .setContentText(subtitle)
+                .setContentIntent(pendingIntent)
+                .setVisibility(visibility)
                 .setPriority(Notification.PRIORITY_HIGH)
-                .build();
-        mNotificationManager.notify(id, notification);
+                .addAction(new Notification.Action.Builder(R.drawable.stat_notify_email,
+                        FIRST_ACTION, emptyIntent)
+                        .build())
+                .addAction(new Notification.Action.Builder(R.drawable.stat_notify_email,
+                        SECOND_ACTION, emptyIntent)
+                        .build())
+                .setAutoCancel(false);
+        if (buzz) {
+            notification.setDefaults(Notification.DEFAULT_VIBRATE);
+        }
+        mNotificationManager.notify(id, notification.build());
         Thread.sleep(LONG_TIMEOUT);
     }
 
-    public void sendNotifications(Map<Integer, String> lists) throws Exception {
+    public void sendNotifications(Map<Integer, String> lists, boolean withDelay) throws Exception {
         Log.v(LOG_TAG, "Sending out notification...");
-        Intent intent = new Intent(Intent.ACTION_VIEW);
-        PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
         CharSequence subtitle = String.valueOf(System.currentTimeMillis());
         for (Map.Entry<Integer, String> l : lists.entrySet()) {
-            Notification notification = new Notification.Builder(mContext)
+            Notification.Builder notification = new Notification.Builder(mContext)
                     .setSmallIcon(R.drawable.stat_notify_email)
                     .setWhen(System.currentTimeMillis()).setContentTitle(l.getValue())
-                    .setContentText(subtitle)
-                    .build();
-            mNotificationManager.notify(l.getKey(), notification);
+                    .setContentTitle(CONTENT_TITLE)
+                    .setContentText(subtitle);
+            mNotificationManager.notify(l.getKey(), notification.build());
+            if (withDelay) {
+                Thread.sleep(SHORT_TIMEOUT);
+            }
         }
         Thread.sleep(LONG_TIMEOUT);
     }
@@ -259,7 +283,7 @@
     }
 
     public void swipeUp() throws Exception {
-        mDevice.swipe(mDevice.getDisplayWidth() / 2, mDevice.getDisplayHeight(),
+        mDevice.swipe(mDevice.getDisplayWidth() / 2, mDevice.getDisplayHeight()*3/4,
                 mDevice.getDisplayWidth() / 2, 0, 30);
         Thread.sleep(SHORT_TIMEOUT);
     }
@@ -279,15 +303,11 @@
         }
     }
 
-    public void showInstalledAppDetails(Context context, String packageName) throws Exception {
-        Intent intent = new Intent();
+    public void showAppNotificationSettings(Context context) throws Exception {
+        Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS);
         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        Uri uri = Uri.fromParts("package", packageName, null);
-        intent.setData(uri);
-        intent.setClassName("com.android.settings",
-                "com.android.settings.Settings$AppNotificationSettingsActivity");
-        intent.putExtra("app_package", mContext.getPackageName());
-        intent.putExtra("app_uid", mContext.getApplicationInfo().uid);
+        intent.putExtra(Settings.EXTRA_APP_PACKAGE, mContext.getPackageName());
+        intent.putExtra(Settings.EXTRA_APP_UID, mContext.getApplicationInfo().uid);
         context.startActivity(intent);
         Thread.sleep(LONG_TIMEOUT * 2);
     }
@@ -337,6 +357,10 @@
         mNotificationManager.notify(notificationId, n.build());
     }
 
+    public NotificationChannel getDefaultChannel() {
+        return mNotificationManager.getNotificationChannel(NotificationChannel.DEFAULT_CHANNEL_ID);
+    }
+
     public static class ToastService extends IntentService {
         private static final String TAG = "ToastService";
         private static final String ACTION_TOAST = "toast";
diff --git a/tests/functional/notificationtests/src/com/android/notification/functional/NotificationInteractionTests.java b/tests/functional/notificationtests/src/com/android/notification/functional/NotificationInteractionTests.java
index 83aae52..53b9c69 100644
--- a/tests/functional/notificationtests/src/com/android/notification/functional/NotificationInteractionTests.java
+++ b/tests/functional/notificationtests/src/com/android/notification/functional/NotificationInteractionTests.java
@@ -16,9 +16,12 @@
 
 package com.android.notification.functional;
 
+import android.app.Notification;
 import android.app.NotificationManager;
 import android.content.Context;
+import android.metrics.LogMaker;
 import android.service.notification.StatusBarNotification;
+import android.support.test.metricshelper.MetricsAsserts;
 import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.Direction;
 import android.support.test.uiautomator.UiDevice;
@@ -28,12 +31,17 @@
 import android.test.suitebuilder.annotation.MediumTest;
 import android.util.Log;
 
+import android.metrics.MetricsReader;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Queue;
 
 public class NotificationInteractionTests extends InstrumentationTestCase {
     private static final String LOG_TAG = NotificationInteractionTests.class.getSimpleName();
-    private static final int LONG_TIMEOUT = 2000;
+    private static final int LONG_TIMEOUT = 3000;
+    private static final int SHORT_TIMEOUT = 200;
     private final boolean DEBUG = false;
     private NotificationManager mNotificationManager;
     private UiDevice mDevice = null;
@@ -41,6 +49,7 @@
     private NotificationHelper mHelper;
     private static final int CUSTOM_NOTIFICATION_ID = 1;
     private static final int NOTIFICATIONS_COUNT = 3;
+    private MetricsReader mMetricsReader;
 
     @Override
     public void setUp() throws Exception {
@@ -52,6 +61,8 @@
         mHelper = new NotificationHelper(mDevice, getInstrumentation(), mNotificationManager);
         mDevice.setOrientationNatural();
         mNotificationManager.cancelAll();
+        mMetricsReader = new MetricsReader();
+        mMetricsReader.checkpoint(); // clear out old logs
     }
 
     @Override
@@ -86,7 +97,8 @@
         for (int i = 0; i < NOTIFICATIONS_COUNT; i++) {
             lists.put(CUSTOM_NOTIFICATION_ID + i, Integer.toString(CUSTOM_NOTIFICATION_ID + i));
         }
-        mHelper.sendNotifications(lists);
+        mHelper.sendNotifications(lists, false);
+
         if (DEBUG) {
             Log.d(LOG_TAG,
                     String.format("posted %s notifications, here they are: ", NOTIFICATIONS_COUNT));
@@ -98,12 +110,166 @@
         if (mDevice.openNotification()) {
             Thread.sleep(LONG_TIMEOUT);
             UiObject2 clearAll = findByText(text);
+            assertNotNull("could not find clear all target", clearAll);
             clearAll.click();
         }
         Thread.sleep(LONG_TIMEOUT);
         sbns = mNotificationManager.getActiveNotifications();
         assertTrue(String.format("%s notifications have not been cleared", sbns.length),
                 sbns.length == currentSbns);
+
+        MetricsAsserts.assertHasVisibilityLog("missing panel revealed log", mMetricsReader,
+                MetricsEvent.NOTIFICATION_PANEL, true);
+        MetricsAsserts.assertHasLog("missing notification visibility log", mMetricsReader,
+                new LogMaker(MetricsEvent.NOTIFICATION_ITEM)
+                        .setType(MetricsEvent.TYPE_OPEN)
+                        .addTaggedData(MetricsEvent.NOTIFICATION_ID, CUSTOM_NOTIFICATION_ID)
+                        .setPackageName(mContext.getPackageName()));
+        MetricsAsserts.assertHasLog("missing notification cancel log", mMetricsReader,
+                new LogMaker(MetricsEvent.NOTIFICATION_ITEM)
+                        .setType(MetricsEvent.TYPE_DISMISS)
+                        .addTaggedData(MetricsEvent.NOTIFICATION_ID, CUSTOM_NOTIFICATION_ID)
+                        .setPackageName(mContext.getPackageName()));
+        MetricsAsserts.assertHasActionLog("missing dismiss-all log", mMetricsReader,
+                MetricsEvent.ACTION_DISMISS_ALL_NOTES);
+        MetricsAsserts.assertHasVisibilityLog("missing panel hidden log", mMetricsReader,
+                MetricsEvent.NOTIFICATION_PANEL, false);
+    }
+
+    /** send notifications, then open and close the shade to test visibility metrics. */
+    @MediumTest
+    public void testNotificationShadeMetricsl() throws Exception {
+        Map<Integer, String> lists = new HashMap<Integer, String>();
+        int firstId = CUSTOM_NOTIFICATION_ID;
+        int secondId = CUSTOM_NOTIFICATION_ID + 1;
+        lists.put(firstId, Integer.toString(firstId));
+        lists.put(secondId, Integer.toString(secondId));
+        // post
+        mHelper.sendNotifications(lists, true);
+        Thread.sleep(LONG_TIMEOUT);
+        // update
+        mHelper.sendNotifications(lists, true);
+
+        if (mDevice.openNotification()) {
+            Thread.sleep(LONG_TIMEOUT);
+        }
+        MetricsAsserts.assertHasVisibilityLog("missing panel revealed log", mMetricsReader,
+                MetricsEvent.NOTIFICATION_PANEL, true);
+        Queue<LogMaker> firstLog = MetricsAsserts.findMatchingLogs(mMetricsReader,
+                new LogMaker(MetricsEvent.NOTIFICATION_ITEM)
+                        .setType(MetricsEvent.TYPE_OPEN)
+                        .addTaggedData(MetricsEvent.NOTIFICATION_ID, firstId)
+                        .setPackageName(mContext.getPackageName()));
+        assertTrue("missing first note visibility log", !firstLog.isEmpty());
+        Queue<LogMaker> secondLog = MetricsAsserts.findMatchingLogs(mMetricsReader,
+                new LogMaker(MetricsEvent.NOTIFICATION_ITEM)
+                        .setType(MetricsEvent.TYPE_OPEN)
+                        .addTaggedData(MetricsEvent.NOTIFICATION_ID, secondId));
+        assertTrue("missing second note visibility log", !secondLog.isEmpty());
+        int firstRank = (Integer) firstLog.peek()
+                .getTaggedData(MetricsEvent.NOTIFICATION_SHADE_INDEX);
+        int secondRank = (Integer) secondLog.peek()
+                .getTaggedData(MetricsEvent.NOTIFICATION_SHADE_INDEX);
+        assertTrue("note must have distinct ranks", firstRank != secondRank);
+        int lifespan = (Integer) firstLog.peek()
+                .getTaggedData(MetricsEvent.NOTIFICATION_SINCE_CREATE_MILLIS);
+        int freshness = (Integer) firstLog.peek()
+                .getTaggedData(MetricsEvent.NOTIFICATION_SINCE_UPDATE_MILLIS);
+        int exposure = (Integer) firstLog.peek()
+                .getTaggedData(MetricsEvent.NOTIFICATION_SINCE_VISIBLE_MILLIS);
+        assertTrue("first note updated before it was created", lifespan >  freshness);
+        assertTrue("first note visible before it was updated", freshness >  exposure);
+        assertTrue("first note visibility log should have zero exposure time", exposure == 0);
+        int secondLifespan = (Integer) secondLog.peek()
+                .getTaggedData(MetricsEvent.NOTIFICATION_SINCE_CREATE_MILLIS);
+        assertTrue("first note created after second note", lifespan >  secondLifespan);
+
+        mMetricsReader.checkpoint(); // clear out old logs again
+        firstLog.clear();
+        secondLog.clear();
+        // close the shade
+        if (mDevice.pressHome()) {
+            Thread.sleep(LONG_TIMEOUT);
+        }
+
+        MetricsAsserts.assertHasVisibilityLog("missing panel hidden log", mMetricsReader,
+                MetricsEvent.NOTIFICATION_PANEL, false);
+        firstLog = MetricsAsserts.findMatchingLogs(mMetricsReader,
+                new LogMaker(MetricsEvent.NOTIFICATION_ITEM)
+                        .setType(MetricsEvent.TYPE_CLOSE)
+                        .addTaggedData(MetricsEvent.NOTIFICATION_ID, firstId)
+                        .setPackageName(mContext.getPackageName()));
+        assertTrue("missing first note hidden log", !firstLog.isEmpty());
+        exposure = (Integer) firstLog.peek()
+                .getTaggedData(MetricsEvent.NOTIFICATION_SINCE_VISIBLE_MILLIS);
+        assertTrue("first note visibility log should have nonzero exposure time", exposure > 0);
+        secondLog = MetricsAsserts.findMatchingLogs(mMetricsReader,
+                new LogMaker(MetricsEvent.NOTIFICATION_ITEM)
+                        .setType(MetricsEvent.TYPE_CLOSE)
+                        .addTaggedData(MetricsEvent.NOTIFICATION_ID, secondId)
+                        .setPackageName(mContext.getPackageName()));
+        assertTrue("missing second note hidden log", !secondLog.isEmpty());
+    }
+
+    /** send a notification, click on first it. */
+    @MediumTest
+    public void testNotificationClicks() throws Exception {
+        int id = CUSTOM_NOTIFICATION_ID;
+        mHelper.sendNotification(id, Notification.VISIBILITY_PUBLIC,
+                NotificationHelper.CONTENT_TITLE, true);
+
+        UiObject2 target = null;
+        if (mDevice.openNotification()) {
+            target = mDevice.wait(
+                    Until.findObject(By.text(NotificationHelper.FIRST_ACTION)),
+                    LONG_TIMEOUT);
+            assertNotNull("could not find first action button", target);
+            target.click();
+        }
+        Thread.sleep(SHORT_TIMEOUT);
+        // top item is always expanded
+        MetricsAsserts.assertHasLog("missing notification expansion log", mMetricsReader,
+                new LogMaker(MetricsEvent.NOTIFICATION_ITEM)
+                        .setType(MetricsEvent.TYPE_DETAIL)
+                        .addTaggedData(MetricsEvent.NOTIFICATION_ID, id)
+                        .setPackageName(mContext.getPackageName()));
+        MetricsAsserts.assertHasLog("missing notification alert log", mMetricsReader,
+                new LogMaker(MetricsEvent.NOTIFICATION_ALERT)
+                        .setType(MetricsEvent.TYPE_OPEN)
+                        .addTaggedData(MetricsEvent.NOTIFICATION_ID, id)
+                        .setSubtype(1) // 1: BUZZ, nop BEEP, nop BLINK
+                        .setPackageName(mContext.getPackageName()));
+        MetricsAsserts.assertHasLog("missing notification action 0 click log", mMetricsReader,
+                new LogMaker(MetricsEvent.NOTIFICATION_ITEM_ACTION)
+                        .setType(MetricsEvent.TYPE_ACTION)
+                        .addTaggedData(MetricsEvent.NOTIFICATION_ID, id)
+                        .setSubtype(0)  // first action button, zero indexed
+                        .setPackageName(mContext.getPackageName()));
+
+        mMetricsReader.checkpoint(); // clear out old logs again
+        target = mDevice.wait(Until.findObject(By.text(NotificationHelper.SECOND_ACTION)),
+                LONG_TIMEOUT);
+        assertNotNull("could not find second action button", target);
+        target.click();
+        Thread.sleep(SHORT_TIMEOUT);
+        MetricsAsserts.assertHasLog("missing notification action 1 click log", mMetricsReader,
+                new LogMaker(MetricsEvent.NOTIFICATION_ITEM_ACTION)
+                        .setType(MetricsEvent.TYPE_ACTION)
+                        .addTaggedData(MetricsEvent.NOTIFICATION_ID, id)
+                        .setSubtype(1)  // second action button, zero indexed
+                        .setPackageName(mContext.getPackageName()));
+
+        mMetricsReader.checkpoint(); // clear out old logs again\
+        target = mDevice.wait(Until.findObject(By.text(NotificationHelper.CONTENT_TITLE)),
+                LONG_TIMEOUT);
+        assertNotNull("could not find content click target", target);
+        target.click();
+        Thread.sleep(SHORT_TIMEOUT);
+        MetricsAsserts.assertHasLog("missing notification content click log", mMetricsReader,
+                new LogMaker(MetricsEvent.NOTIFICATION_ITEM)
+                        .setType(MetricsEvent.TYPE_ACTION)
+                        .addTaggedData(MetricsEvent.NOTIFICATION_ID, id)
+                        .setPackageName(mContext.getPackageName()));
     }
 
     private UiObject2 findByText(String text) throws Exception {
diff --git a/tests/functional/otatests/Android.mk b/tests/functional/otatests/Android.mk
deleted file mode 100644
index 2d6a980..0000000
--- a/tests/functional/otatests/Android.mk
+++ /dev/null
@@ -1,16 +0,0 @@
-
-LOCAL_PATH := $(call my-dir)
-
-include $(CLEAR_VARS)
-
-LOCAL_PACKAGE_NAME := OtaFunctionalTests
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
-LOCAL_MODULE_TAGS := tests
-LOCAL_STATIC_JAVA_LIBRARIES := \
-    android-support-test \
-    easymocklib \
-    launcher-helper-lib \
-    objenesis-target \
-    ub-uiautomator
-
-include $(BUILD_PACKAGE)
diff --git a/tests/functional/otatests/AndroidManifest.xml b/tests/functional/otatests/AndroidManifest.xml
deleted file mode 100644
index b4037ea..0000000
--- a/tests/functional/otatests/AndroidManifest.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-        package="com.android.functional.otatests">
-        
-    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
-    
-    <uses-sdk android:minSdkVersion="19"
-        android:targetSdkVersion="24" />
-
-    <application>
-        <uses-library android:name="android.test.runner" />
-    </application>
-
-    <instrumentation
-        android:name="android.support.test.runner.AndroidJUnitRunner"
-        android:targetPackage="com.android.functional.otatests"
-        android:label="Android System Update Functional Tests" />
-</manifest>
diff --git a/tests/functional/otatests/src/com/android/functional/otatests/PackageProcessTest.java b/tests/functional/otatests/src/com/android/functional/otatests/PackageProcessTest.java
deleted file mode 100644
index f3a873a..0000000
--- a/tests/functional/otatests/src/com/android/functional/otatests/PackageProcessTest.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.functional.otatests;
-
-import static org.easymock.EasyMock.createNiceMock;
-import static org.junit.Assert.assertTrue;
-
-import android.app.Instrumentation;
-import android.content.Context;
-import android.content.ContextWrapper;
-import android.os.IPowerManager;
-import android.os.PowerManager;
-import android.os.RecoverySystem;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.runner.AndroidJUnit4;
-import java.io.File;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@RunWith(AndroidJUnit4.class)
-public class PackageProcessTest {
-
-    private static final String PACKAGE_DATA_PATH =
-            "/data/data/com.google.android.gms/app_download/update.zip";
-    private static final String BLOCK_MAP = "/cache/recovery/block.map";
-    private static final String UNCRYPT_FILE = "/cache/recovery/uncrypt_file";
-
-    private Context mMockContext;
-    private Context mContext;
-    private PowerManager mMockPowerManager;
-    private Instrumentation mInstrumentation;
-
-    private class PackageProcessMockContext extends ContextWrapper {
-
-        private Context mInternal;
-
-        public PackageProcessMockContext(Context base) {
-            super(base);
-            mInternal = base;
-        }
-
-        @Override
-        public Object getSystemService(String name) {
-            if (name.equals(Context.POWER_SERVICE)) {
-                return mMockPowerManager;
-            }
-            return mInternal.getSystemService(name);
-        }
-    }
-
-    @Before
-    public void setUp() throws Exception {
-        mInstrumentation = InstrumentationRegistry.getInstrumentation();
-        mContext = mInstrumentation.getContext();
-        mMockContext = new PackageProcessMockContext(mContext);
-        // Set a mocked out power manager into the mocked context, so the device
-        // won't reboot at the end of installPackage
-        IPowerManager mockIPowerManager = createNiceMock(IPowerManager.class);
-        mMockPowerManager = new PowerManager(mContext, mockIPowerManager, null);
-    }
-
-    @Test
-    public void testPackageProcessOnly() throws Exception {
-        File pkg = new File(PACKAGE_DATA_PATH);
-        RecoverySystem.verifyPackage(pkg, null, null);
-        RecoverySystem.processPackage(mMockContext, pkg, null);
-        // uncrypt will push block.map to this location if and only if it finishes successfully
-        assertTrue(new File(BLOCK_MAP).exists());
-    }
-
-    @Test
-    public void testPackageProcessViaInstall() throws Exception {
-        File pkg = new File(PACKAGE_DATA_PATH);
-        RecoverySystem.verifyPackage(pkg, null, null);
-        RecoverySystem.installPackage(mMockContext, pkg);
-        // uncrypt will push block.map to this location if and only if it finishes successfully
-        assertTrue(new File(UNCRYPT_FILE).exists());
-    }
-}
diff --git a/tests/functional/otatests/src/com/android/functional/otatests/SystemUpdateAppTest.java b/tests/functional/otatests/src/com/android/functional/otatests/SystemUpdateAppTest.java
deleted file mode 100644
index 12e0158..0000000
--- a/tests/functional/otatests/src/com/android/functional/otatests/SystemUpdateAppTest.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.functional.otatests;
-
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-
-import android.app.Instrumentation;
-import android.content.Intent;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.runner.AndroidJUnit4;
-import android.support.test.uiautomator.By;
-import android.support.test.uiautomator.UiDevice;
-import android.support.test.uiautomator.Until;
-import org.junit.After;
-import org.junit.BeforeClass;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Tests functionality related to the System Update app.
- *
- * Precondition: an OTA must be available to the device.
- */
-@RunWith(AndroidJUnit4.class)
-public class SystemUpdateAppTest {
-
-    private static final String SYSTEM_UPDATE_INTENT = "android.settings.SYSTEM_UPDATE_SETTINGS";
-    private static final String POLICY_AUTH_PACKAGE = "com.google.android.gms.policy_auth";
-    private static final long TIMEOUT_MS = 2000;
-
-    private UiDevice mDevice;
-    private Instrumentation mInstrumentation;
-
-    @BeforeClass
-    public void setUp() throws Exception {
-        mInstrumentation = InstrumentationRegistry.getInstrumentation();
-        mDevice = UiDevice.getInstance(mInstrumentation);
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        mDevice.pressHome();
-    }
-
-    @Test
-    public void testUpdateIsAvailable() {
-        mInstrumentation.getContext().startActivity(new Intent(SYSTEM_UPDATE_INTENT));
-        String mainText = mDevice.wait(Until.findObject(
-                By.res(POLICY_AUTH_PACKAGE, "suw_layout_title")), TIMEOUT_MS).getText();
-        assertTrue(mainText.contains("available"));
-        assertNotNull(mDevice.findObject(By.res(POLICY_AUTH_PACKAGE, "suw_items_title")));
-    }
-}
diff --git a/tests/functional/otatests/src/com/android/functional/otatests/SystemUpdateTest.java b/tests/functional/otatests/src/com/android/functional/otatests/SystemUpdateTest.java
deleted file mode 100644
index 4d5acec..0000000
--- a/tests/functional/otatests/src/com/android/functional/otatests/SystemUpdateTest.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.functional.otatests;
-
-import org.junit.Test;
-
-/**
- * A basic test case to assert that the system was updated to the expected version.
- */
-public class SystemUpdateTest extends VersionCheckingTest {
-
-    public SystemUpdateTest(String testPath) {
-        super(testPath);
-    }
-
-    @Test
-    public void testIsUpdated() throws Exception {
-        assertUpdated();
-    }
-}
diff --git a/tests/functional/otatests/src/com/android/functional/otatests/VersionCheckingTest.java b/tests/functional/otatests/src/com/android/functional/otatests/VersionCheckingTest.java
deleted file mode 100644
index df3df9c..0000000
--- a/tests/functional/otatests/src/com/android/functional/otatests/VersionCheckingTest.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.functional.otatests;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-
-import android.os.Bundle;
-import android.support.test.InstrumentationRegistry;
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.util.Arrays;
-import org.junit.Before;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
-
-@RunWith(Parameterized.class)
-public class VersionCheckingTest {
-
-    protected static final String OLD_VERSION = "/sdcard/otatest/version.old";
-    protected static final String NEW_VERSION = "/sdcard/otatest/version.new";
-    protected static final String KEY_BUILD_ID = "ro.build.version.incremental";
-    protected static final String KEY_BOOTLOADER = "ro.bootloader";
-    protected static final String KEY_BASEBAND = "ro.build.expect.baseband";
-    protected static final String KEY_BASEBAND_GSM = "gsm.version.baseband";
-    protected static final String PATH_NAME = "path_name";
-
-    protected VersionInfo mOldVersion;
-    protected VersionInfo mNewVersion;
-
-    protected String mTestPath;
-
-    public VersionCheckingTest(String testPath) {
-        mTestPath = testPath;
-    }
-
-    @Before
-    public void setUp() throws Exception {
-        try {
-            mOldVersion = VersionInfo.parseFromFile(OLD_VERSION);
-            mNewVersion = VersionInfo.parseFromFile(NEW_VERSION);
-        } catch (IOException e) {
-            throw new RuntimeException(
-                    "Couldn't find version file; was this test run with VersionCachePreparer?", e);
-        }
-    }
-
-    @Parameters(name = "{0}")
-    public static Iterable<? extends Object> getOtaPathName() {
-        Bundle args = InstrumentationRegistry.getArguments();
-        if (args.containsKey(PATH_NAME)) {
-            return Arrays.asList(args.getString(PATH_NAME));
-        }
-        return Arrays.asList("unnamed path");
-    }
-
-    protected void assertNotUpdated() throws IOException {
-        assertEquals(mOldVersion.getBuildId(), getProp(KEY_BUILD_ID));
-        assertEquals(mOldVersion.getBootloaderVersion(), getProp(KEY_BOOTLOADER));
-        assertTrue(mOldVersion.getBasebandVersion().equals(getProp(KEY_BASEBAND))
-                || mOldVersion.getBasebandVersion().equals(getProp(KEY_BASEBAND_GSM)));
-    }
-
-    protected void assertUpdated() throws IOException {
-        assertEquals(mNewVersion.getBuildId(), getProp(KEY_BUILD_ID));
-        assertEquals(mNewVersion.getBootloaderVersion(), getProp(KEY_BOOTLOADER));
-        // Due to legacy property names (an old meaning to gsm.version.baseband),
-        // the KEY_BASEBAND and KEY_BASEBAND_GSM properties may not match each other.
-        // At least one of them will always match the baseband version recorded by
-        // NEW_VERSION.
-        assertTrue(mNewVersion.getBasebandVersion().equals(getProp(KEY_BASEBAND))
-                || mNewVersion.getBasebandVersion().equals(getProp(KEY_BASEBAND_GSM)));
-    }
-
-    private String getProp(String key) throws IOException {
-        Process p = Runtime.getRuntime().exec("getprop " + key);
-        BufferedReader r = new BufferedReader(new InputStreamReader(p.getInputStream()));
-        String ret = r.readLine().trim();
-        r.close();
-        return ret;
-    }
-}
diff --git a/tests/functional/otatests/src/com/android/functional/otatests/VersionInfo.java b/tests/functional/otatests/src/com/android/functional/otatests/VersionInfo.java
deleted file mode 100644
index 624c698..0000000
--- a/tests/functional/otatests/src/com/android/functional/otatests/VersionInfo.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.functional.otatests;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStreamReader;
-
-public class VersionInfo {
-    private final String mBuildId;
-    private final String mBootloaderVersion;
-    private final String mBasebandVersion;
-
-    private VersionInfo(String buildId, String bootVersion, String radioVersion) {
-        mBuildId = buildId;
-        mBootloaderVersion = bootVersion;
-        mBasebandVersion = radioVersion;
-    }
-
-    public String getBuildId() {
-        return mBuildId;
-    }
-
-    public String getBootloaderVersion() {
-        return mBootloaderVersion;
-    }
-
-    public String getBasebandVersion() {
-        return mBasebandVersion;
-    }
-
-    public static VersionInfo parseFromFile(String fileName) throws IOException {
-        BufferedReader r = new BufferedReader(
-                new InputStreamReader(new FileInputStream(new File(fileName))));
-        try {
-            return new VersionInfo(
-                    denull(r.readLine()),
-                    denull(r.readLine()),
-                    denull(r.readLine()));
-        } finally {
-            r.close();
-        }
-    }
-
-    private static String denull(String s) {
-        return  s == null || s.equals("null") ? "" : s;
-    }
-}
diff --git a/tests/functional/overviewtests/Android.mk b/tests/functional/overviewtests/Android.mk
index f2ca59f..32a69e8 100644
--- a/tests/functional/overviewtests/Android.mk
+++ b/tests/functional/overviewtests/Android.mk
@@ -29,4 +29,6 @@
 
 LOCAL_SDK_VERSION := current
 
+LOCAL_COMPATIBILITY_SUITE := device-tests
+
 include $(BUILD_PACKAGE)
diff --git a/tests/functional/overviewtests/AndroidTest.xml b/tests/functional/overviewtests/AndroidTest.xml
new file mode 100644
index 0000000..8a992a7
--- /dev/null
+++ b/tests/functional/overviewtests/AndroidTest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Runs Platform Android Overview Functional Tests.">
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+        <option name="test-file-name" value="OverviewFunctionalTests.apk" />
+    </target_preparer>
+
+    <option name="test-tag" value="android_systemui" />
+    <test class="com.android.tradefed.testtype.InstrumentationTest" >
+        <option name="package" value="android.overview.functional" />
+        <option name="runner" value="android.support.test.runner.AndroidJUnitRunner" />
+    </test>
+</configuration>
diff --git a/tests/functional/overviewtests/src/com/android/overview/functional/MultiWindowTests.java b/tests/functional/overviewtests/src/com/android/overview/functional/MultiWindowTests.java
index 4cbf7e7..db9fcd7 100644
--- a/tests/functional/overviewtests/src/com/android/overview/functional/MultiWindowTests.java
+++ b/tests/functional/overviewtests/src/com/android/overview/functional/MultiWindowTests.java
@@ -97,6 +97,9 @@
     public void testResizeHandleOnMultiwindow() throws Exception {
         mOverviewHelper.dockAppsToBothMultiwindowAreas(CALCULATOR_PACKAGE, "Calculator",
                 GMAIL_PACKAGE);
+        // Adding a sleep here to make sure the test fetches the bounds of the
+        // elements on the multiwindow screen instead of the home screen.
+        Thread.sleep(TIMEOUT);
         // verify initial bounds for top and bottom
         mDevice.click(mDevice.getDisplayHeight() / 4, mDevice.getDisplayWidth() / 2);
         UiObject2 calcArea = mDevice.wait(Until.findObject
diff --git a/tests/functional/permission/Android.mk b/tests/functional/permission/Android.mk
index a41443e..89c5de6 100644
--- a/tests/functional/permission/Android.mk
+++ b/tests/functional/permission/Android.mk
@@ -10,9 +10,13 @@
 LOCAL_STATIC_JAVA_LIBRARIES := \
     ub-uiautomator \
     launcher-helper-lib \
+    permission-helper \
+    package-helper \
     junit
 
 LOCAL_PACKAGE_NAME := PermissionFunctionalTests
 LOCAL_CERTIFICATE := platform
 
+LOCAL_COMPATIBILITY_SUITE := device-tests
+
 include $(BUILD_PACKAGE)
diff --git a/tests/functional/permission/AndroidTest.xml b/tests/functional/permission/AndroidTest.xml
new file mode 100644
index 0000000..2640372
--- /dev/null
+++ b/tests/functional/permission/AndroidTest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Runs Permission Functional Tests.">
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+        <option name="test-file-name" value="PermissionFunctionalTests.apk" />
+        <option name="test-file-name" value="PermissionTestAppMV1.apk" />
+    </target_preparer>
+
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-tag" value="PermissionTests" />
+    <test class="com.android.tradefed.testtype.InstrumentationTest" >
+        <option name="package" value="com.android.functional.permissiontests" />
+        <option name="runner" value="android.test.InstrumentationTestRunner" />
+    </test>
+</configuration>
diff --git a/tests/functional/permission/src/com/android/functional/permissiontests/GenericAppPermissionTests.java b/tests/functional/permission/src/com/android/functional/permissiontests/GenericAppPermissionTests.java
index 1923e3c..3379dca 100644
--- a/tests/functional/permission/src/com/android/functional/permissiontests/GenericAppPermissionTests.java
+++ b/tests/functional/permission/src/com/android/functional/permissiontests/GenericAppPermissionTests.java
@@ -18,19 +18,19 @@
 
 import android.app.UiAutomation;
 import android.content.Context;
-import android.os.RemoteException;
+import android.os.SystemClock;
+import android.support.test.launcherhelper.ILauncherStrategy;
 import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.BySelector;
 import android.support.test.uiautomator.UiDevice;
 import android.support.test.uiautomator.UiObject2;
 import android.support.test.uiautomator.Until;
+import android.system.helpers.PackageHelper;
+import android.system.helpers.PermissionHelper;
 import android.test.InstrumentationTestCase;
 import android.test.suitebuilder.annotation.MediumTest;
 import android.test.suitebuilder.annotation.SmallTest;
 
-import com.android.functional.permissiontests.PermissionHelper.PermissionOp;
-import com.android.functional.permissiontests.PermissionHelper.PermissionStatus;
-
 import java.util.Arrays;
 import java.util.List;
 
@@ -42,7 +42,9 @@
     private UiDevice mDevice = null;
     private Context mContext = null;
     private UiAutomation mUiAutomation = null;
-    private PermissionHelper pHelper;
+    private PermissionHelper pHelper = null;
+    private PackageHelper pkgHelper = null;
+    private ILauncherStrategy mILauncherStrategy = null;
     private final String[] mDefaultPermittedGroups = new String[] {
             "CONTACTS", "SMS", "STORAGE"
     };
@@ -61,7 +63,8 @@
         mContext = getInstrumentation().getContext();
         mUiAutomation = getInstrumentation().getUiAutomation();
         mDevice.setOrientationNatural();
-        pHelper = PermissionHelper.getInstance(mDevice, mContext, mUiAutomation);
+        pHelper = PermissionHelper.getInstance();
+        pkgHelper = PackageHelper.getInstance(getInstrumentation());
         mDefaultGrantedPermissions = pHelper.getPermissionByPackage(TARGET_APP_PKG, Boolean.TRUE);
     }
 
@@ -84,12 +87,13 @@
     public void testToggleAppPermisssionOFF() {
         pHelper.togglePermissionSetting(PERMISSION_TEST_APP, "Contacts", Boolean.FALSE);
         pHelper.verifyPermissionSettingStatus(
-                PERMISSION_TEST_APP, "Contacts", PermissionStatus.OFF);
+                PERMISSION_TEST_APP, "Contacts", PermissionHelper.PermissionStatus.OFF);
     }
 
     public void testToggleAppPermisssionON() {
         pHelper.togglePermissionSetting(PERMISSION_TEST_APP, "Contacts", Boolean.TRUE);
-        pHelper.verifyPermissionSettingStatus(PERMISSION_TEST_APP, "Contacts", PermissionStatus.ON);
+        pHelper.verifyPermissionSettingStatus(PERMISSION_TEST_APP, "Contacts",
+                PermissionHelper.PermissionStatus.ON);
     }
 
     @MediumTest
@@ -101,8 +105,10 @@
     }
 
     public void testPermissionDialogAllow() {
-        pHelper.cleanPackage(PERMISSION_TEST_APP_PKG);
-        pHelper.launchApp(PERMISSION_TEST_APP_PKG, PERMISSION_TEST_APP);
+        pkgHelper.cleanPackage(PERMISSION_TEST_APP_PKG);
+        if (!mDevice.hasObject(By.pkg(PERMISSION_TEST_APP_PKG).depth(0))) {
+            mILauncherStrategy.launch(PERMISSION_TEST_APP, PERMISSION_TEST_APP_PKG);
+        }
         mDevice.wait(Until.findObject(By.text("GET CONTACT PERMISSION")), pHelper.TIMEOUT).click();
         mDevice.wait(Until.findObject(
                 By.res(PACKAGE_INSTALLER, "permission_allow_button")), pHelper.TIMEOUT).click();
@@ -112,10 +118,13 @@
     }
 
     public void testPermissionDialogDenyFlow() {
-        pHelper.cleanPackage(PERMISSION_TEST_APP_PKG);
-        pHelper.launchApp(PERMISSION_TEST_APP_PKG, PERMISSION_TEST_APP);
+        pkgHelper.cleanPackage(PERMISSION_TEST_APP_PKG);
+        if (!mDevice.hasObject(By.pkg(PERMISSION_TEST_APP_PKG).depth(0))) {
+            mILauncherStrategy.launch(PERMISSION_TEST_APP, PERMISSION_TEST_APP_PKG);
+        }
         pHelper.grantOrRevokePermissionViaAdb(
-                PERMISSION_TEST_APP_PKG, "android.permission.READ_CONTACTS", PermissionOp.REVOKE);
+                PERMISSION_TEST_APP_PKG, "android.permission.READ_CONTACTS",
+                PermissionHelper.PermissionOp.REVOKE);
         BySelector getContactSelector = By.text("GET CONTACT PERMISSION");
         BySelector dontAskChkSelector = By.res(PACKAGE_INSTALLER, "do_not_ask_checkbox");
         BySelector denySelctor = By.res(PACKAGE_INSTALLER, "permission_deny_button");
diff --git a/libraries/base-app-helpers/Android.mk b/tests/functional/systemmetrics/Android.mk
similarity index 66%
copy from libraries/base-app-helpers/Android.mk
copy to tests/functional/systemmetrics/Android.mk
index 0111a0a..d1a51ff 100644
--- a/libraries/base-app-helpers/Android.mk
+++ b/tests/functional/systemmetrics/Android.mk
@@ -1,4 +1,3 @@
-#
 # Copyright (C) 2016 The Android Open Source Project
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,13 +11,23 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-#
+
 LOCAL_PATH := $(call my-dir)
-
 include $(CLEAR_VARS)
-LOCAL_MODULE := base-app-helpers
-LOCAL_STATIC_JAVA_LIBRARIES := dpad-util
-LOCAL_JAVA_LIBRARIES := ub-uiautomator launcher-helper-lib
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
-include $(BUILD_STATIC_JAVA_LIBRARY)
+LOCAL_PACKAGE_NAME := SystemMetricsFunctionalTests
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_MODULE_TAGS := tests
+LOCAL_STATIC_JAVA_LIBRARIES := \
+    android-support-test \
+    metrics-helper-lib \
+    ub-uiautomator \
+    services.core
+
+LOCAL_JAVA_LIBRARIES := legacy-android-test
+
+#LOCAL_SDK_VERSION := current
+
+LOCAL_COMPATIBILITY_SUITE := device-tests
+
+include $(BUILD_PACKAGE)
diff --git a/tests/functional/systemmetrics/AndroidManifest.xml b/tests/functional/systemmetrics/AndroidManifest.xml
new file mode 100644
index 0000000..aacbb80
--- /dev/null
+++ b/tests/functional/systemmetrics/AndroidManifest.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT 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.android.systemmetrics.functional" >
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <uses-sdk
+        android:minSdkVersion="21"
+        android:targetSdkVersion="24" />
+
+    <uses-permission android:name="android.permission.READ_LOGS" />
+
+    <instrumentation
+        android:name="android.support.test.runner.AndroidJUnitRunner"
+        android:label="Android System Metrics Functional Tests"
+        android:targetPackage="com.android.systemmetrics.functional" />
+
+</manifest>
diff --git a/tests/functional/systemmetrics/AndroidTest.xml b/tests/functional/systemmetrics/AndroidTest.xml
new file mode 100644
index 0000000..86d342f
--- /dev/null
+++ b/tests/functional/systemmetrics/AndroidTest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Runs Android System Metrics Functional Tests.">
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+        <option name="test-file-name" value="SystemMetricsFunctionalTests.apk" />
+    </target_preparer>
+
+    <option name="test-tag" value="SystemMetricsFunctionalTests" />
+    <test class="com.android.tradefed.testtype.InstrumentationTest" >
+        <option name="package" value="com.android.systemmetrics.functional" />
+        <option name="runner" value="android.support.test.runner.AndroidJUnitRunner" />
+    </test>
+</configuration>
diff --git a/tests/functional/systemmetrics/src/com/android/systemmetrics/functional/AppStartTests.java b/tests/functional/systemmetrics/src/com/android/systemmetrics/functional/AppStartTests.java
new file mode 100644
index 0000000..43c6681
--- /dev/null
+++ b/tests/functional/systemmetrics/src/com/android/systemmetrics/functional/AppStartTests.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemmetrics.functional;
+
+import android.content.Context;
+import android.content.Intent;
+import android.metrics.LogMaker;
+import android.metrics.MetricsReader;
+import android.os.SystemClock;
+import android.support.test.metricshelper.MetricsAsserts;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.Until;
+import android.telecom.Log;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.text.TextUtils;
+
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+
+import java.util.Queue;
+
+/**
+ * runtest --path platform_testing/tests/functional/systemmetrics/
+ */
+public class AppStartTests extends InstrumentationTestCase {
+    private static final String LOG_TAG = AppStartTests.class.getSimpleName();
+    private static final String SETTINGS_PACKAGE = "com.android.settings";
+    private static final int LONG_TIMEOUT_MS = 2000;
+    private UiDevice mDevice = null;
+    private Context mContext;
+    private MetricsReader mMetricsReader;
+    private int mPreUptime;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mDevice = UiDevice.getInstance(getInstrumentation());
+        mContext = getInstrumentation().getContext();
+        mDevice.setOrientationNatural();
+        mMetricsReader = new MetricsReader();
+        mMetricsReader.checkpoint(); // clear out old logs
+        mPreUptime = (int) (SystemClock.uptimeMillis() / 1000);
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        super.tearDown();
+        mDevice.unfreezeRotation();
+        mDevice.pressHome();
+    }
+
+    @MediumTest
+    public void testStartApp() throws Exception {
+        Context context = getInstrumentation().getContext();
+        Intent intent = context.getPackageManager().getLaunchIntentForPackage(SETTINGS_PACKAGE);
+
+        // Clear out any previous instances
+        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
+
+        assertNotNull("component name is null", intent.getComponent());
+        String className = intent.getComponent().getClassName();
+        String packageName = intent.getComponent().getPackageName();
+        assertTrue("className is empty", !TextUtils.isEmpty(className));
+        assertTrue("packageName is empty", !TextUtils.isEmpty(packageName));
+
+
+        context.startActivity(intent);
+        mDevice.wait(Until.hasObject(By.pkg(SETTINGS_PACKAGE).depth(0)), LONG_TIMEOUT_MS);
+
+        int postUptime = (int) (SystemClock.uptimeMillis() / 1000);
+
+        Queue<LogMaker> startLogs = MetricsAsserts.findMatchingLogs(mMetricsReader,
+                new LogMaker(MetricsEvent.APP_TRANSITION));
+        boolean found = false;
+        for (LogMaker log : startLogs) {
+            String actualClassName = (String) log.getTaggedData(
+                    MetricsEvent.FIELD_CLASS_NAME);
+            String actualPackageName = log.getPackageName();
+            if (className.equals(actualClassName) && packageName.equals(actualPackageName)) {
+                found = true;
+                int startUptime = ((Number)
+                        log.getTaggedData(MetricsEvent.APP_TRANSITION_DEVICE_UPTIME_SECONDS))
+                        .intValue();
+                assertTrue("must be either cold or warm launch",
+                        MetricsEvent.TYPE_TRANSITION_COLD_LAUNCH == log.getType()
+                                || MetricsEvent.TYPE_TRANSITION_WARM_LAUNCH == log.getType());
+                assertTrue("reported uptime should be after the app was started",
+                        mPreUptime <= startUptime);
+                assertTrue("reported uptime should be before assertion time",
+                        startUptime <= postUptime);
+                assertNotNull("log should have delay",
+                        log.getTaggedData(MetricsEvent.APP_TRANSITION_DELAY_MS));
+                assertEquals("transition should be started because of starting window",
+                        1 /* APP_TRANSITION_STARTING_WINDOW */, log.getSubtype());
+                assertNotNull("log should have starting window delay",
+                        log.getTaggedData(MetricsEvent.APP_TRANSITION_STARTING_WINDOW_DELAY_MS));
+                assertNotNull("log should have windows drawn delay",
+                        log.getTaggedData(MetricsEvent.APP_TRANSITION_WINDOWS_DRAWN_DELAY_MS));
+            }
+        }
+        assertTrue("did not find the app start start log for: "
+                + intent.getComponent().flattenToShortString(), found);
+    }
+}
diff --git a/tests/functional/testapks/applinktestapp/Android.mk b/tests/functional/testapks/applinktestapp/Android.mk
index 08c75f8..cc3831e 100644
--- a/tests/functional/testapks/applinktestapp/Android.mk
+++ b/tests/functional/testapks/applinktestapp/Android.mk
@@ -23,4 +23,7 @@
 LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
 LOCAL_PACKAGE_NAME := AppLinkTestApp
 LOCAL_CERTIFICATE := platform
+
+LOCAL_COMPATIBILITY_SUITE := device-tests
+
 include $(BUILD_PACKAGE)
diff --git a/tests/functional/testapks/permissiontestappmv1/Android.mk b/tests/functional/testapks/permissiontestappmv1/Android.mk
index 0bf4196..f9586be 100644
--- a/tests/functional/testapks/permissiontestappmv1/Android.mk
+++ b/tests/functional/testapks/permissiontestappmv1/Android.mk
@@ -11,4 +11,7 @@
     $(LOCAL_PATH)/res
 LOCAL_PACKAGE_NAME := PermissionTestAppMV1
 LOCAL_CERTIFICATE := platform
+
+LOCAL_COMPATIBILITY_SUITE := device-tests
+
 include $(BUILD_PACKAGE)
diff --git a/tests/jank/UbSystemUiJankTests/Android.mk b/tests/jank/UbSystemUiJankTests/Android.mk
index eb5e92c..de87765 100644
--- a/tests/jank/UbSystemUiJankTests/Android.mk
+++ b/tests/jank/UbSystemUiJankTests/Android.mk
@@ -19,8 +19,11 @@
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 LOCAL_MODULE_TAGS := tests
 
-LOCAL_STATIC_JAVA_LIBRARIES := ub-janktesthelper ub-uiautomator launcher-helper-lib timeresult-helper-lib
+LOCAL_STATIC_JAVA_LIBRARIES := ub-janktesthelper ub-uiautomator launcher-helper-lib \
+    timeresult-helper-lib sysui-helper
 
-LOCAL_SDK_VERSION := 21
+LOCAL_JAVA_LIBRARIES := legacy-android-test
+
+LOCAL_COMPATIBILITY_SUITE := device-tests
 
 include $(BUILD_PACKAGE)
diff --git a/tests/jank/UbSystemUiJankTests/AndroidTest.xml b/tests/jank/UbSystemUiJankTests/AndroidTest.xml
new file mode 100644
index 0000000..cefe1c8
--- /dev/null
+++ b/tests/jank/UbSystemUiJankTests/AndroidTest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Runs System UI Jank Tests.">
+    <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+        <option name="push" value="/google/data/ro/teams/tradefed/testdata/browser/chrome-command-line->/data/local/chrome-command-line" />
+        <option name="push" value="/google/data/ro/teams/tradefed/testdata/browser/chrome-command-line->/data/local/tmp/chrome-command-line" />
+    </target_preparer>
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+        <option name="test-file-name" value="UbSystemUiJankTests.apk" />
+    </target_preparer>
+
+    <option name="test-tag" value="UbSystemUiJankTests" />
+    <test class="com.android.tradefed.testtype.InstrumentationTest" >
+        <option name="package" value="android.platform.systemui.tests.jank" />
+        <option name="runner" value="android.test.InstrumentationTestRunner" />
+    </test>
+</configuration>
diff --git a/tests/jank/UbSystemUiJankTests/src/android/platform/systemui/tests/jank/SystemUiJankTests.java b/tests/jank/UbSystemUiJankTests/src/android/platform/systemui/tests/jank/SystemUiJankTests.java
index 62c4ebd..82b2446 100644
--- a/tests/jank/UbSystemUiJankTests/src/android/platform/systemui/tests/jank/SystemUiJankTests.java
+++ b/tests/jank/UbSystemUiJankTests/src/android/platform/systemui/tests/jank/SystemUiJankTests.java
@@ -16,17 +16,20 @@
 
 package android.platform.systemui.tests.jank;
 
+import android.app.Notification.Action;
 import android.app.Notification.Builder;
 import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.RemoteInput;
 import android.content.Context;
 import android.content.Intent;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
 import android.graphics.Rect;
+import android.graphics.drawable.Icon;
 import android.os.Bundle;
 import android.os.Environment;
 import android.os.RemoteException;
 import android.os.SystemClock;
+import android.support.test.InstrumentationRegistry;
 import android.support.test.jank.GfxMonitor;
 import android.support.test.jank.JankTest;
 import android.support.test.jank.JankTestBase;
@@ -38,7 +41,8 @@
 import android.support.test.uiautomator.UiObject2;
 import android.support.test.uiautomator.UiSelector;
 import android.support.test.uiautomator.Until;
-import android.util.Log;
+import android.system.helpers.LockscreenHelper;
+import android.system.helpers.OverviewHelper;
 import android.widget.Button;
 import android.widget.ImageView;
 
@@ -50,11 +54,14 @@
 public class SystemUiJankTests extends JankTestBase {
 
     private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
+    private static final String SETTINGS_PACKAGE = "com.android.settings";
     private static final BySelector RECENTS = By.res(SYSTEMUI_PACKAGE, "recents_view");
     private static final String LOG_TAG = SystemUiJankTests.class.getSimpleName();
     private static final int SWIPE_MARGIN = 5;
     private static final int DEFAULT_FLING_STEPS = 5;
     private static final int DEFAULT_SCROLL_STEPS = 15;
+    private static final int BRIGHTNESS_SCROLL_STEPS = 30;
+
     // short transitions should be repeated within the test function, otherwise frame stats
     // captured are not really meaningful in a statistical sense
     private static final int INNER_LOOP = 3;
@@ -72,6 +79,7 @@
             android.R.drawable.stat_notify_voicemail,
     };
     private static final String NOTIFICATION_TEXT = "Lorem ipsum dolor sit amet";
+    private static final String REPLY_TEXT = "REPLY";
     private static final File TIMESTAMP_FILE = new File(Environment.getExternalStorageDirectory()
             .getAbsolutePath(), "autotester.log");
     private static final File RESULTS_FILE = new File(Environment.getExternalStorageDirectory()
@@ -79,17 +87,40 @@
     private static final String GMAIL_PACKAGE_NAME = "com.google.android.gm";
     private static final String DISABLE_COMMAND = "pm disable-user ";
     private static final String ENABLE_COMMAND = "pm enable ";
+    private static final String PULSE_COMMAND = "am broadcast -a com.android.systemui.doze.pulse";
+    private static final String PIN = "1234";
+
+    /**
+     * Group mode: Let the system auto-group our notifications. This is required so we don't screw
+     * up jank numbers for our existing notification list pull test.
+     */
+    private static final int GROUP_MODE_LEGACY = 0;
+
+    /**
+     * Group mode: Group the notifications.
+     */
+    private static final int GROUP_MODE_GROUPED = 1;
+
+    /**
+     * Group mode: All notifications should be separate
+     */
+    private static final int GROUP_MODE_UNGROUPED = 2;
 
     private UiDevice mDevice;
-    private List<String> mLaunchedPackages = new ArrayList<>();
+    private ArrayList<String> mLaunchedPackages;
+    private NotificationManager mNotificationManager;
 
-    public void setUp() {
+    public void setUp() throws Exception {
         mDevice = UiDevice.getInstance(getInstrumentation());
         try {
             mDevice.setOrientationNatural();
         } catch (RemoteException e) {
             throw new RuntimeException("failed to freeze device orientaion", e);
         }
+        mNotificationManager = getInstrumentation().getContext().getSystemService(
+                NotificationManager.class);
+        InstrumentationRegistry.registerInstance(getInstrumentation(), getArguments());
+        blockNotifications();
     }
 
     public void goHome() {
@@ -100,32 +131,12 @@
     @Override
     protected void tearDown() throws Exception {
         mDevice.unfreezeRotation();
+        unblockNotifications();
         super.tearDown();
     }
 
     public void populateRecentApps() throws IOException {
-        PackageManager pm = getInstrumentation().getContext().getPackageManager();
-        List<PackageInfo> packages = pm.getInstalledPackages(0);
-        mLaunchedPackages.clear();
-        for (PackageInfo pkg : packages) {
-            if (pkg.packageName.equals(getInstrumentation().getTargetContext().getPackageName())) {
-                continue;
-            }
-            Intent intent = pm.getLaunchIntentForPackage(pkg.packageName);
-            if (intent == null) {
-                continue;
-            }
-            intent.addCategory(Intent.CATEGORY_LAUNCHER);
-            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-            getInstrumentation().getTargetContext().startActivity(intent);
-            SystemClock.sleep(5000);
-            mLaunchedPackages.add(pkg.packageName);
-        }
-
-        // Close any crash dialogs
-        while (mDevice.hasObject(By.textContains("has stopped"))) {
-            mDevice.findObject(By.text("Close")).clickAndWait(Until.newWindow(), 2000);
-        }
+        mLaunchedPackages = OverviewHelper.getInstance().populateManyRecentApps();
         TimeResultLogger.writeTimeStampLogStart(String.format("%s-%s",
                 getClass().getSimpleName(), getName()), TIMESTAMP_FILE);
     }
@@ -133,13 +144,7 @@
     public void forceStopPackages(Bundle metrics) throws IOException {
         TimeResultLogger.writeTimeStampLogEnd(String.format("%s-%s",
                 getClass().getSimpleName(), getName()), TIMESTAMP_FILE);
-        for (String pkg : mLaunchedPackages) {
-            try {
-                mDevice.executeShellCommand("am force-stop " + pkg);
-            } catch (IOException e) {
-                Log.w(LOG_TAG, "exeception while force stopping package " + pkg, e);
-            }
-        }
+        OverviewHelper.getInstance().forceStopPackages(mLaunchedPackages);
         goHome();
         TimeResultLogger.writeResultToFile(String.format("%s-%s",
                 getClass().getSimpleName(), getName()), RESULTS_FILE, metrics);
@@ -160,8 +165,7 @@
         mDevice.waitForIdle();
     }
 
-    public void prepareNotifications() throws Exception {
-        blockNotifications();
+    public void prepareNotifications(int groupMode) throws Exception {
         goHome();
         mDevice.openNotification();
         SystemClock.sleep(100);
@@ -175,19 +179,66 @@
         }
         mDevice.pressHome();
         mDevice.waitForIdle();
+        postNotifications(groupMode);
+        mDevice.waitForIdle();
+    }
+
+    private void postNotifications(int groupMode) {
+        postNotifications(groupMode, 100, -1);
+    }
+
+    private void postNotifications(int groupMode, int sleepBetweenDuration, int maxCount) {
         Builder builder = new Builder(getInstrumentation().getTargetContext())
                 .setContentTitle(NOTIFICATION_TEXT);
-        NotificationManager nm = (NotificationManager) getInstrumentation().getTargetContext()
-                .getSystemService(Context.NOTIFICATION_SERVICE);
-        for (int icon : ICONS) {
+        if (groupMode == GROUP_MODE_GROUPED) {
+            builder.setGroup("key");
+        }
+        boolean first = true;
+        for (int i = 0; i < ICONS.length; i++) {
+            if (maxCount != -1 && i >= maxCount) {
+                break;
+            }
+            int icon = ICONS[i];
+            if (first && groupMode == GROUP_MODE_GROUPED) {
+                builder.setGroupSummary(true);
+            } else {
+                builder.setGroupSummary(false);
+            }
+            if (groupMode == GROUP_MODE_UNGROUPED) {
+                builder.setGroup(Integer.toString(icon));
+            }
             builder.setContentText(Integer.toHexString(icon))
                     .setSmallIcon(icon);
-            nm.notify(icon, builder.build());
-            SystemClock.sleep(100);
+            mNotificationManager.notify(icon, builder.build());
+            SystemClock.sleep(sleepBetweenDuration);
+            first = false;
         }
-        mDevice.waitForIdle();
-        TimeResultLogger.writeTimeStampLogStart(String.format("%s-%s",
-                getClass().getSimpleName(), getName()), TIMESTAMP_FILE);
+    }
+
+    private void postInlineReplyNotification() {
+        RemoteInput remoteInput = new RemoteInput.Builder("reply")
+                .setLabel(NOTIFICATION_TEXT)
+                .build();
+        Context context = getInstrumentation().getTargetContext();
+        PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0 , new Intent(),
+                PendingIntent.FLAG_UPDATE_CURRENT);
+        Icon icon = Icon.createWithResource(context, ICONS[0]);
+        Action action = new Action.Builder(icon, REPLY_TEXT, pendingIntent)
+                .addRemoteInput(remoteInput)
+                .build();
+        Builder builder = new Builder(getInstrumentation().getTargetContext())
+                .setContentTitle(NOTIFICATION_TEXT)
+                .setContentText(NOTIFICATION_TEXT)
+                .setSmallIcon(ICONS[0])
+                .addAction(action);
+        mNotificationManager.notify(0, builder.build());
+    }
+
+    private void cancelNotifications(int sleepBetweenDuration) {
+        for (int icon : ICONS) {
+            mNotificationManager.cancel(icon);
+            SystemClock.sleep(sleepBetweenDuration);
+        }
     }
 
     public void blockNotifications() throws Exception {
@@ -198,21 +249,13 @@
         mDevice.executeShellCommand(ENABLE_COMMAND + GMAIL_PACKAGE_NAME);
     }
 
-    public void cancelNotifications(Bundle metrics) throws Exception {
-        unblockNotifications();
-        TimeResultLogger.writeTimeStampLogEnd(String.format("%s-%s",
-                getClass().getSimpleName(), getName()), TIMESTAMP_FILE);
-        NotificationManager nm = (NotificationManager) getInstrumentation().getTargetContext()
-                .getSystemService(Context.NOTIFICATION_SERVICE);
-        nm.cancelAll();
-        TimeResultLogger.writeResultToFile(String.format("%s-%s",
-                getClass().getSimpleName(), getName()), RESULTS_FILE, metrics);
-        super.afterTest(metrics);
+    public void cancelNotifications() throws Exception {
+        mNotificationManager.cancelAll();
     }
 
     /** Starts from the bottom of the recent apps list and measures jank while flinging up. */
     @JankTest(beforeTest = "populateRecentApps", beforeLoop = "resetRecentsToBottom",
-            afterTest = "forceStopPackages", expectedFrames = 100)
+            afterTest = "forceStopPackages", expectedFrames = 100, defaultIterationCount = 5)
     @GfxMonitor(processName = SYSTEMUI_PACKAGE)
     public void testRecentAppsFling() {
         UiObject2 recents = mDevice.findObject(RECENTS);
@@ -228,6 +271,28 @@
         }
     }
 
+    /**
+     * Measures jank when dismissing a task in recents.
+     */
+    @JankTest(beforeTest = "populateRecentApps", beforeLoop = "resetRecentsToBottom",
+            afterTest = "forceStopPackages", expectedFrames = 10, defaultIterationCount = 5)
+    @GfxMonitor(processName = SYSTEMUI_PACKAGE)
+    public void testRecentAppsDismiss() {
+        // Wait until dismiss views are fully faded in.
+        mDevice.findObject(new UiSelector().resourceId("com.android.systemui:id/dismiss_task"))
+                .waitForExists(5000);
+        for (int i = 0; i < INNER_LOOP; i++) {
+            List<UiObject2> dismissViews = mDevice.findObjects(
+                    By.res(SYSTEMUI_PACKAGE, "dismiss_task"));
+            if (dismissViews.size() == 0) {
+                fail("Unable to find dismiss view");
+            }
+            dismissViews.get(dismissViews.size() - 1).click();
+            mDevice.waitForIdle();
+            SystemClock.sleep(500);
+        }
+    }
+
     private void swipeDown() {
         mDevice.swipe(mDevice.getDisplayWidth() / 2,
                 SWIPE_MARGIN, mDevice.getDisplayWidth() / 2,
@@ -243,9 +308,25 @@
                 DEFAULT_SCROLL_STEPS);
     }
 
+    public void beforeNotificationListPull() throws Exception {
+        prepareNotifications(GROUP_MODE_LEGACY);
+        TimeResultLogger.writeTimeStampLogStart(String.format("%s-%s",
+                getClass().getSimpleName(), getName()), TIMESTAMP_FILE);
+    }
+
+    public void afterNotificationListPull(Bundle metrics) throws Exception {
+        TimeResultLogger.writeTimeStampLogEnd(String.format("%s-%s",
+                getClass().getSimpleName(), getName()), TIMESTAMP_FILE);
+        cancelNotifications();
+        TimeResultLogger.writeResultToFile(String.format("%s-%s",
+                getClass().getSimpleName(), getName()), RESULTS_FILE, metrics);
+        super.afterTest(metrics);
+    }
+
     /** Measures jank while pulling down the notification list */
     @JankTest(expectedFrames = 100,
-            beforeTest = "prepareNotifications", afterTest = "cancelNotifications")
+            defaultIterationCount = 5,
+            beforeTest = "beforeNotificationListPull", afterTest = "afterNotificationListPull")
     @GfxMonitor(processName = SYSTEMUI_PACKAGE)
     public void testNotificationListPull() {
         for (int i = 0; i < INNER_LOOP; i++) {
@@ -256,10 +337,31 @@
         }
     }
 
+    public void beforeNotificationListPull_manyNotifications() throws Exception {
+        prepareNotifications(GROUP_MODE_UNGROUPED);
+        TimeResultLogger.writeTimeStampLogStart(String.format("%s-%s",
+                getClass().getSimpleName(), getName()), TIMESTAMP_FILE);
+    }
+
+    /** Measures jank while pulling down the notification list with many notifications */
+    @JankTest(expectedFrames = 100,
+            defaultIterationCount = 5,
+            beforeTest = "beforeNotificationListPull_manyNotifications",
+            afterTest = "afterNotificationListPull")
+    @GfxMonitor(processName = SYSTEMUI_PACKAGE)
+    public void testNotificationListPull_manyNotifications() {
+        for (int i = 0; i < INNER_LOOP; i++) {
+            swipeDown();
+            mDevice.waitForIdle();
+            swipeUp();
+            mDevice.waitForIdle();
+        }
+    }
+
     public void beforeQuickSettings() throws Exception {
 
         // Make sure we have some notifications.
-        prepareNotifications();
+        prepareNotifications(GROUP_MODE_UNGROUPED);
         mDevice.openNotification();
         SystemClock.sleep(100);
         mDevice.waitForIdle();
@@ -270,7 +372,7 @@
     public void afterQuickSettings(Bundle metrics) throws Exception {
         TimeResultLogger.writeTimeStampLogEnd(String.format("%s-%s",
                 getClass().getSimpleName(), getName()), TIMESTAMP_FILE);
-        cancelNotifications(metrics);
+        cancelNotifications();
         mDevice.pressHome();
         TimeResultLogger.writeResultToFile(String.format("%s-%s",
                 getClass().getSimpleName(), getName()), RESULTS_FILE, metrics);
@@ -279,6 +381,7 @@
 
     /** Measures jank while pulling down the quick settings */
     @JankTest(expectedFrames = 100,
+            defaultIterationCount = 5,
             beforeTest = "beforeQuickSettings", afterTest = "afterQuickSettings")
     @GfxMonitor(processName = SYSTEMUI_PACKAGE)
     public void testQuickSettingsPull() throws Exception {
@@ -292,5 +395,457 @@
             mDevice.waitForIdle();
         }
     }
-}
 
+    public void beforeUnlock() throws Exception {
+
+        // Make sure we have some notifications.
+        prepareNotifications(GROUP_MODE_UNGROUPED);
+        TimeResultLogger.writeTimeStampLogStart(String.format("%s-%s",
+                getClass().getSimpleName(), getName()), TIMESTAMP_FILE);
+    }
+
+    public void afterUnlock(Bundle metrics) throws Exception {
+        TimeResultLogger.writeTimeStampLogEnd(String.format("%s-%s",
+                getClass().getSimpleName(), getName()), TIMESTAMP_FILE);
+        cancelNotifications();
+        mDevice.pressHome();
+        TimeResultLogger.writeResultToFile(String.format("%s-%s",
+                getClass().getSimpleName(), getName()), RESULTS_FILE, metrics);
+        super.afterTest(metrics);
+    }
+
+    /**
+     * Measure jank while unlocking the phone.
+     */
+    @JankTest(expectedFrames = 100,
+            defaultIterationCount = 5,
+            beforeTest = "beforeUnlock", afterTest = "afterUnlock")
+    @GfxMonitor(processName = SYSTEMUI_PACKAGE)
+    public void testUnlock() throws Exception {
+        for (int i = 0; i < INNER_LOOP; i++) {
+            mDevice.sleep();
+            // Make sure we don't trigger the camera launch double-tap shortcut
+            SystemClock.sleep(300);
+            mDevice.wakeUp();
+            swipeUp();
+            mDevice.waitForIdle();
+        }
+    }
+
+    public void beforeExpand() throws Exception {
+        prepareNotifications(GROUP_MODE_GROUPED);
+        mDevice.openNotification();
+        SystemClock.sleep(100);
+        mDevice.waitForIdle();
+        TimeResultLogger.writeTimeStampLogStart(String.format("%s-%s",
+                getClass().getSimpleName(), getName()), TIMESTAMP_FILE);
+    }
+
+    public void afterExpand(Bundle metrics) throws Exception {
+        TimeResultLogger.writeTimeStampLogEnd(String.format("%s-%s",
+                getClass().getSimpleName(), getName()), TIMESTAMP_FILE);
+        cancelNotifications();
+        mDevice.pressHome();
+        TimeResultLogger.writeResultToFile(String.format("%s-%s",
+                getClass().getSimpleName(), getName()), RESULTS_FILE, metrics);
+        super.afterTest(metrics);
+    }
+
+    /**
+     * Measures jank while expending a group notification.
+     */
+    @JankTest(expectedFrames = 100,
+            defaultIterationCount = 5,
+            beforeTest = "beforeExpand", afterTest = "afterExpand")
+    @GfxMonitor(processName = SYSTEMUI_PACKAGE)
+    public void testExpandGroup() throws Exception {
+        UiObject expandButton = mDevice.findObject(
+                new UiSelector().resourceId("android:id/expand_button"));
+        for (int i = 0; i < INNER_LOOP; i++) {
+            expandButton.click();
+            mDevice.waitForIdle();
+            expandButton.click();
+            mDevice.waitForIdle();
+        }
+    }
+
+    private void scrollDown() {
+        mDevice.swipe(mDevice.getDisplayWidth() / 2,
+                mDevice.getDisplayHeight() / 2,
+                mDevice.getDisplayWidth() / 2,
+                SWIPE_MARGIN,
+                DEFAULT_SCROLL_STEPS);
+    }
+
+    public void beforeClearAll() throws Exception {
+        TimeResultLogger.writeTimeStampLogStart(String.format("%s-%s",
+                getClass().getSimpleName(), getName()), TIMESTAMP_FILE);
+    }
+
+    public void beforeClearAllLoop() throws Exception {
+        postNotifications(GROUP_MODE_UNGROUPED);
+        mDevice.openNotification();
+        SystemClock.sleep(100);
+        mDevice.waitForIdle();
+    }
+
+    public void afterClearAll(Bundle metrics) throws Exception {
+        TimeResultLogger.writeTimeStampLogEnd(String.format("%s-%s",
+                getClass().getSimpleName(), getName()), TIMESTAMP_FILE);
+        TimeResultLogger.writeResultToFile(String.format("%s-%s",
+                getClass().getSimpleName(), getName()), RESULTS_FILE, metrics);
+        super.afterTest(metrics);
+    }
+
+    /**
+     * Measures jank when clicking the "clear all" button in the notification shade.
+     */
+    @JankTest(expectedFrames = 10,
+            defaultIterationCount = 5,
+            beforeTest = "beforeClearAll",
+            beforeLoop = "beforeClearAllLoop",
+            afterTest = "afterClearAll")
+    @GfxMonitor(processName = SYSTEMUI_PACKAGE)
+    public void testClearAll() throws Exception {
+        UiObject clearAll =
+                mDevice.findObject(new UiSelector().className(Button.class).text("CLEAR ALL"));
+        while (!clearAll.exists()) {
+            scrollDown();
+        }
+        clearAll.click();
+        mDevice.waitForIdle();
+    }
+
+    public void beforeChangeBrightness() throws Exception {
+        mDevice.openQuickSettings();
+
+        // Wait until animation is starting.
+        SystemClock.sleep(200);
+        mDevice.waitForIdle();
+        TimeResultLogger.writeTimeStampLogStart(String.format("%s-%s",
+                getClass().getSimpleName(), getName()), TIMESTAMP_FILE);
+    }
+
+    public void afterChangeBrightness(Bundle metrics) throws Exception {
+        TimeResultLogger.writeTimeStampLogEnd(String.format("%s-%s",
+                getClass().getSimpleName(), getName()), TIMESTAMP_FILE);
+        mDevice.pressHome();
+        TimeResultLogger.writeResultToFile(String.format("%s-%s",
+                getClass().getSimpleName(), getName()), RESULTS_FILE, metrics);
+        super.afterTest(metrics);
+    }
+
+    /**
+     * Measures jank when changing screen brightness
+     */
+    @JankTest(expectedFrames = 10,
+            defaultIterationCount = 5,
+            beforeTest = "beforeChangeBrightness",
+            afterTest = "afterChangeBrightness")
+    @GfxMonitor(processName = SYSTEMUI_PACKAGE)
+    public void testChangeBrightness() throws Exception {
+        UiObject2 brightness = mDevice.findObject(By.res(SYSTEMUI_PACKAGE, "slider"));
+        Rect bounds = brightness.getVisibleBounds();
+        for (int i = 0; i < INNER_LOOP; i++) {
+            mDevice.swipe(bounds.left, bounds.centerY(),
+                    bounds.right, bounds.centerY(), BRIGHTNESS_SCROLL_STEPS);
+
+            // Make sure animation is completing.
+            SystemClock.sleep(500);
+            mDevice.waitForIdle();
+        }
+    }
+
+    public void beforeNotificationAppear() throws Exception {
+        mDevice.openNotification();
+
+        // Wait until animation is starting.
+        SystemClock.sleep(200);
+        mDevice.waitForIdle();
+        TimeResultLogger.writeTimeStampLogStart(String.format("%s-%s",
+                getClass().getSimpleName(), getName()), TIMESTAMP_FILE);
+    }
+
+    public void afterNotificationAppear(Bundle metrics) throws Exception {
+        TimeResultLogger.writeTimeStampLogEnd(String.format("%s-%s",
+                getClass().getSimpleName(), getName()), TIMESTAMP_FILE);
+        mDevice.pressHome();
+        TimeResultLogger.writeResultToFile(String.format("%s-%s",
+                getClass().getSimpleName(), getName()), RESULTS_FILE, metrics);
+        super.afterTest(metrics);
+    }
+
+    /**
+     * Measures jank when a notification is appearing.
+     */
+    @JankTest(expectedFrames = 10,
+            defaultIterationCount = 5,
+            beforeTest = "beforeNotificationAppear",
+            afterTest = "afterNotificationAppear")
+    @GfxMonitor(processName = SYSTEMUI_PACKAGE)
+    public void testNotificationAppear() throws Exception {
+        for (int i = 0; i < INNER_LOOP; i++) {
+            postNotifications(GROUP_MODE_UNGROUPED, 250, 5);
+            mDevice.waitForIdle();
+            cancelNotifications(250);
+            mDevice.waitForIdle();
+        }
+    }
+
+    public void beforeCameraFromLockscreen() throws Exception {
+        TimeResultLogger.writeTimeStampLogStart(String.format("%s-%s",
+                getClass().getSimpleName(), getName()), TIMESTAMP_FILE);
+    }
+
+    public void beforeCameraFromLockscreenLoop() throws Exception {
+        mDevice.pressHome();
+        mDevice.sleep();
+        // Make sure we don't trigger the camera launch double-tap shortcut
+        SystemClock.sleep(300);
+        mDevice.wakeUp();
+        mDevice.waitForIdle();
+    }
+
+    public void afterCameraFromLockscreen(Bundle metrics) throws Exception {
+        TimeResultLogger.writeTimeStampLogEnd(String.format("%s-%s",
+                getClass().getSimpleName(), getName()), TIMESTAMP_FILE);
+        mDevice.pressHome();
+        TimeResultLogger.writeResultToFile(String.format("%s-%s",
+                getClass().getSimpleName(), getName()), RESULTS_FILE, metrics);
+        super.afterTest(metrics);
+    }
+
+    /**
+     * Measures jank when launching the camera from lockscreen.
+     */
+    @JankTest(expectedFrames = 10,
+            defaultIterationCount = 5,
+            beforeTest = "beforeCameraFromLockscreen",
+            afterTest = "afterCameraFromLockscreen",
+            beforeLoop = "beforeCameraFromLockscreenLoop")
+    @GfxMonitor(processName = SYSTEMUI_PACKAGE)
+    public void testCameraFromLockscreen() throws Exception {
+        mDevice.swipe(mDevice.getDisplayWidth() - SWIPE_MARGIN,
+                mDevice.getDisplayHeight() - SWIPE_MARGIN, SWIPE_MARGIN, SWIPE_MARGIN,
+                DEFAULT_SCROLL_STEPS);
+        mDevice.waitForIdle();
+    }
+
+    public void beforeAmbientWakeUp() throws Exception {
+        postNotifications(GROUP_MODE_UNGROUPED);
+        mDevice.sleep();
+        SystemClock.sleep(1000);
+        mDevice.waitForIdle();
+        TimeResultLogger.writeTimeStampLogStart(String.format("%s-%s",
+                getClass().getSimpleName(), getName()), TIMESTAMP_FILE);
+    }
+
+    public void afterAmbientWakeUp(Bundle metrics) throws Exception {
+        TimeResultLogger.writeTimeStampLogEnd(String.format("%s-%s",
+                getClass().getSimpleName(), getName()), TIMESTAMP_FILE);
+        cancelNotifications();
+        mDevice.wakeUp();
+        mDevice.waitForIdle();
+        mDevice.pressMenu();
+        mDevice.waitForIdle();
+        TimeResultLogger.writeResultToFile(String.format("%s-%s",
+                getClass().getSimpleName(), getName()), RESULTS_FILE, metrics);
+        super.afterTest(metrics);
+    }
+
+    /**
+     * Measures jank when waking up from ambient (doze) display.
+     */
+    @JankTest(expectedFrames = 30,
+            defaultIterationCount = 5,
+            beforeTest = "beforeAmbientWakeUp",
+            afterTest = "afterAmbientWakeUp")
+    @GfxMonitor(processName = SYSTEMUI_PACKAGE)
+    public void testAmbientWakeUp() throws Exception {
+        for (int i = 0; i < INNER_LOOP; i++) {
+            mDevice.executeShellCommand(PULSE_COMMAND);
+            SystemClock.sleep(100);
+            mDevice.waitForIdle();
+            mDevice.wakeUp();
+            mDevice.waitForIdle();
+            mDevice.sleep();
+            SystemClock.sleep(1000);
+            mDevice.waitForIdle();
+        }
+    }
+
+    public void beforeGoToFullShade() throws Exception {
+        postNotifications(GROUP_MODE_UNGROUPED);
+        mDevice.sleep();
+
+        // Don't trigger camera launch gesture
+        SystemClock.sleep(300);
+        mDevice.wakeUp();
+        mDevice.waitForIdle();
+        TimeResultLogger.writeTimeStampLogStart(String.format("%s-%s",
+                getClass().getSimpleName(), getName()), TIMESTAMP_FILE);
+    }
+
+    public void afterGoToFullShade(Bundle metrics) throws Exception {
+        TimeResultLogger.writeTimeStampLogEnd(String.format("%s-%s",
+                getClass().getSimpleName(), getName()), TIMESTAMP_FILE);
+        mDevice.pressMenu();
+        mDevice.waitForIdle();
+        cancelNotifications();
+        TimeResultLogger.writeResultToFile(String.format("%s-%s",
+                getClass().getSimpleName(), getName()), RESULTS_FILE, metrics);
+        super.afterTest(metrics);
+    }
+
+    /**
+     * Measures jank when tragging down on a notification on the lockscreen to go to the full shade.
+     */
+    @JankTest(expectedFrames = 100,
+            defaultIterationCount = 5,
+            beforeTest = "beforeGoToFullShade",
+            afterTest = "afterGoToFullShade")
+    @GfxMonitor(processName = SYSTEMUI_PACKAGE)
+    public void testGoToFullShade() throws Exception {
+        for (int i = 0; i < INNER_LOOP; i++) {
+            mDevice.swipe(mDevice.getDisplayWidth() / 2, mDevice.getDisplayHeight() / 2,
+                    mDevice.getDisplayWidth() / 2, mDevice.getDisplayHeight() - SWIPE_MARGIN,
+                    DEFAULT_SCROLL_STEPS);
+            mDevice.waitForIdle();
+            mDevice.click(mDevice.getDisplayWidth() / 4, mDevice.getDisplayHeight() - SWIPE_MARGIN);
+            mDevice.waitForIdle();
+        }
+    }
+
+    public void beforeInlineReply() throws Exception {
+        postInlineReplyNotification();
+        mDevice.openNotification();
+
+        // Wait until animation kicks in
+        SystemClock.sleep(100);
+        mDevice.waitForIdle();
+        TimeResultLogger.writeTimeStampLogStart(String.format("%s-%s",
+                getClass().getSimpleName(), getName()), TIMESTAMP_FILE);
+    }
+
+    public void afterInlineReply(Bundle metrics) throws Exception {
+        TimeResultLogger.writeTimeStampLogEnd(String.format("%s-%s",
+                getClass().getSimpleName(), getName()), TIMESTAMP_FILE);
+        cancelNotifications();
+        goHome();
+        TimeResultLogger.writeResultToFile(String.format("%s-%s",
+                getClass().getSimpleName(), getName()), RESULTS_FILE, metrics);
+        super.afterTest(metrics);
+    }
+
+    /**
+     * Measures jank when clicking "reply" on a notification that supports inline reply.
+     */
+    @JankTest(expectedFrames = 50,
+            defaultIterationCount = 5,
+            beforeTest = "beforeInlineReply",
+            afterTest = "afterInlineReply")
+    @GfxMonitor(processName = SYSTEMUI_PACKAGE)
+    public void testInlineReply() throws Exception {
+        UiObject2 replyButton = mDevice.findObject(By.clazz(Button.class).text(REPLY_TEXT));
+        for (int i = 0; i < INNER_LOOP; i++) {
+            replyButton.click();
+            mDevice.waitForIdle();
+            Thread.sleep(1000);
+            mDevice.pressBack();
+            mDevice.waitForIdle();
+            mDevice.pressBack();
+            mDevice.waitForIdle();
+        }
+    }
+
+    public void beforePinAppearance() throws Exception {
+        LockscreenHelper.getInstance().setScreenLockViaShell(PIN, LockscreenHelper.MODE_PIN);
+        goHome();
+        mDevice.sleep();
+        SystemClock.sleep(300);
+        mDevice.wakeUp();
+        TimeResultLogger.writeTimeStampLogStart(String.format("%s-%s",
+                getClass().getSimpleName(), getName()), TIMESTAMP_FILE);
+    }
+
+    public void afterPinAppearanceLoop() throws Exception {
+        mDevice.pressBack();
+        mDevice.waitForIdle();
+    }
+
+    public void afterPinAppearance(Bundle metrics) throws Exception {
+        TimeResultLogger.writeTimeStampLogEnd(String.format("%s-%s",
+                getClass().getSimpleName(), getName()), TIMESTAMP_FILE);
+        LockscreenHelper.getInstance().unlockScreen(PIN);
+        LockscreenHelper.getInstance().removeScreenLockViaShell(PIN);
+        mDevice.pressHome();
+        TimeResultLogger.writeResultToFile(String.format("%s-%s",
+                getClass().getSimpleName(), getName()), RESULTS_FILE, metrics);
+        super.afterTest(metrics);
+    }
+
+    /**
+     * Measures jank when launching the camera from lockscreen.
+     */
+    @JankTest(expectedFrames = 30,
+            defaultIterationCount = 5,
+            beforeTest = "beforePinAppearance",
+            afterTest = "afterPinAppearance",
+            afterLoop = "afterPinAppearanceLoop")
+    @GfxMonitor(processName = SYSTEMUI_PACKAGE)
+    public void testPinAppearance() throws Exception {
+        mDevice.swipe(mDevice.getDisplayWidth() / 2, mDevice.getDisplayHeight() - SWIPE_MARGIN,
+                mDevice.getDisplayWidth() / 2, mDevice.getDisplayHeight() / 2,
+                DEFAULT_SCROLL_STEPS);
+        mDevice.waitForIdle();
+        String command = String.format("%s %s %s", "input", "text", PIN);
+        mDevice.executeShellCommand(command);
+        mDevice.waitForIdle();
+    }
+
+    public void beforeLaunchSettings() throws Exception {
+        prepareNotifications(GROUP_MODE_UNGROUPED);
+        TimeResultLogger.writeTimeStampLogStart(String.format("%s-%s",
+                getClass().getSimpleName(), getName()), TIMESTAMP_FILE);
+    }
+
+    public void beforeLaunchSettingsLoop() throws Exception {
+        mDevice.openNotification();
+
+        // Wait until animation kicks in
+        SystemClock.sleep(100);
+        mDevice.waitForIdle();
+    }
+
+    public void afterLaunchSettingsLoop() throws Exception {
+        mDevice.executeShellCommand("am force-stop " + SETTINGS_PACKAGE);
+        mDevice.pressHome();
+        mDevice.waitForIdle();
+    }
+
+    public void afterLaunchSettings(Bundle metrics) throws Exception {
+        TimeResultLogger.writeTimeStampLogEnd(String.format("%s-%s",
+                getClass().getSimpleName(), getName()), TIMESTAMP_FILE);
+        cancelNotifications();
+        TimeResultLogger.writeResultToFile(String.format("%s-%s",
+                getClass().getSimpleName(), getName()), RESULTS_FILE, metrics);
+        super.afterTest(metrics);
+    }
+
+    /**
+     * Measures jank when launching settings from notification shade
+     */
+    @JankTest(expectedFrames = 30,
+            defaultIterationCount = 5,
+            beforeTest = "beforeLaunchSettings",
+            afterTest = "afterLaunchSettings",
+            beforeLoop = "beforeLaunchSettingsLoop",
+            afterLoop = "afterLaunchSettingsLoop")
+    @GfxMonitor(processName = SYSTEMUI_PACKAGE)
+    public void testLaunchSettings() throws Exception {
+        mDevice.findObject(By.res(SYSTEMUI_PACKAGE, "settings_button")).click();
+        // Wait until animation kicks in
+        SystemClock.sleep(100);
+        mDevice.waitForIdle();
+    }
+}
diff --git a/tests/jank/androidtvjanktests/Android.mk b/tests/jank/androidtvjanktests/Android.mk
index f01074f..70ebaf3 100644
--- a/tests/jank/androidtvjanktests/Android.mk
+++ b/tests/jank/androidtvjanktests/Android.mk
@@ -19,8 +19,11 @@
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 LOCAL_MODULE_TAGS := tests
 
-LOCAL_STATIC_JAVA_LIBRARIES := ub-janktesthelper ub-uiautomator timeresult-helper-lib
+LOCAL_STATIC_JAVA_LIBRARIES := \
+    ub-janktesthelper ub-uiautomator timeresult-helper-lib dpad-util
 
 LOCAL_SDK_VERSION := 21
 
+LOCAL_COMPATIBILITY_SUITE := device-tests
+
 include $(BUILD_PACKAGE)
\ No newline at end of file
diff --git a/tests/jank/androidtvjanktests/AndroidTest.xml b/tests/jank/androidtvjanktests/AndroidTest.xml
new file mode 100644
index 0000000..db45c38
--- /dev/null
+++ b/tests/jank/androidtvjanktests/AndroidTest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Runs Platform Android TV Jank Tests.">
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+        <option name="test-file-name" value="AndroidTVJankTests.apk" />
+    </target_preparer>
+
+    <option name="test-tag" value="AndroidTVJankTests" />
+    <test class="com.android.tradefed.testtype.InstrumentationTest" >
+        <option name="package" value="com.android.androidtv.janktests" />
+        <option name="runner" value="android.test.InstrumentationTestRunner" />
+    </test>
+</configuration>
diff --git a/tests/jank/androidtvjanktests/src/com/android/androidtv/janktests/SystemAppJankTests.java b/tests/jank/androidtvjanktests/src/com/android/androidtv/janktests/SystemAppJankTests.java
index ccd913c..f6747d0 100644
--- a/tests/jank/androidtvjanktests/src/com/android/androidtv/janktests/SystemAppJankTests.java
+++ b/tests/jank/androidtvjanktests/src/com/android/androidtv/janktests/SystemAppJankTests.java
@@ -24,12 +24,11 @@
 import android.support.test.jank.JankTest;
 import android.support.test.jank.JankTestBase;
 import android.support.test.uiautomator.By;
-import android.support.test.uiautomator.Direction;
 import android.support.test.uiautomator.UiDevice;
 import android.support.test.uiautomator.UiObject2;
 import android.support.test.uiautomator.UiObjectNotFoundException;
 import android.support.test.uiautomator.Until;
-import android.util.Log;
+
 import java.io.IOException;
 
 /*
diff --git a/tests/jank/androidtvjanktests/src/com/android/androidtv/janktests/SystemUiJankTests.java b/tests/jank/androidtvjanktests/src/com/android/androidtv/janktests/SystemUiJankTests.java
index 444c19b..6868b76 100644
--- a/tests/jank/androidtvjanktests/src/com/android/androidtv/janktests/SystemUiJankTests.java
+++ b/tests/jank/androidtvjanktests/src/com/android/androidtv/janktests/SystemUiJankTests.java
@@ -17,17 +17,21 @@
 package com.android.androidtv.janktests;
 
 import android.os.Bundle;
-import android.os.SystemClock;
+import android.platform.test.utils.DPadUtil;
 import android.support.test.jank.GfxMonitor;
 import android.support.test.jank.JankTest;
 import android.support.test.jank.JankTestBase;
 import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
 import android.support.test.uiautomator.Direction;
 import android.support.test.uiautomator.UiDevice;
 import android.support.test.uiautomator.UiObject2;
 import android.support.test.uiautomator.UiObjectNotFoundException;
 import android.support.test.uiautomator.Until;
 import android.util.Log;
+
+import junit.framework.Assert;
+
 import java.io.IOException;
 
 /*
@@ -35,17 +39,20 @@
  */
 public class SystemUiJankTests extends JankTestBase {
 
+    private static final String TAG = SystemUiJankTests.class.getSimpleName();
     private static final int SHORT_TIMEOUT = 1000;
-    private static final int LONG_TIMEOUT = 3000;
     private static final int INNER_LOOP = 4;
-    private static final int FLING_SPEED = 12000;
-    private static final String LEANBACK_LAUNCHER = "com.google.android.leanbacklauncher";
+    private static final int INNER_LOOP_SETTINGS = 8;
+    private static final String TVLAUNCHER_PACKAGE = "com.google.android.tvlauncher";
     private static final String SETTINGS_PACKAGE = "com.android.tv.settings";
+    private static final BySelector SELECTOR_TOP_ROW = By.res(TVLAUNCHER_PACKAGE, "top_row");
     private UiDevice mDevice;
+    private DPadUtil mDPadUtil;
 
     @Override
     public void setUp() {
         mDevice = UiDevice.getInstance(getInstrumentation());
+        mDPadUtil = new DPadUtil(getInstrumentation());
     }
 
     @Override
@@ -55,43 +62,38 @@
 
     public void goHome() {
         mDevice.pressHome();
-        // Ensure that Home screen is being displayed
-        UiObject2 homeScreen = mDevice.wait(
-                Until.findObject(By.scrollable(true).res(LEANBACK_LAUNCHER, "main_list_view")),
+        UiObject2 homeScreen = mDevice
+            .wait(Until.findObject(By.res(TVLAUNCHER_PACKAGE, "home_view_container")),
                 SHORT_TIMEOUT);
+        Assert.assertNotNull("Ensure that Home screen is being displayed", homeScreen);
+    }
+
+    public void goTopRow() {
+        Assert.assertNotNull(select(SELECTOR_TOP_ROW.hasDescendant(By.focused(true)), Direction.UP,
+                SHORT_TIMEOUT));
     }
 
     public void afterTestHomeScreenNavigation(Bundle metrics) throws IOException {
         super.afterTest(metrics);
     }
 
-    // Measures jank while scrolling down the Home screen
+    // Measures jank while navigating up and down the Home screen
     @JankTest(expectedFrames=100, beforeTest = "goHome",
             afterTest="afterTestHomeScreenNavigation")
-    @GfxMonitor(processName=LEANBACK_LAUNCHER)
+    @GfxMonitor(processName=TVLAUNCHER_PACKAGE)
     public void testHomeScreenNavigation() throws UiObjectNotFoundException {
         // We've already verified that Home screen is being displayed.
-        // Scroll up and down the home screen.
-        navigateDownAndUpCurrentScreen();
+        // Navigate up and down the home screen.
+        navigateDownAndUpCurrentScreen(INNER_LOOP);
     }
 
-    // Navigates to the Settings row on the Home screen
-    public void goToSettingsRow() {
+    // Navigates to the Settings button on the Top row
+    public void goToSettingsButton() {
         // Navigate to Home screen and verify that it is being displayed.
         goHome();
-        mDevice.wait(Until.findObject(By.scrollable(true).res(LEANBACK_LAUNCHER, "main_list_view")),
-                SHORT_TIMEOUT);
-        // Look for the row with 'Settings' text.
-        // This will ensure that the DPad focus is on the Settings icon.
-        int count = 0;
-        while (count <= 5 && !(mDevice.hasObject(By.res(LEANBACK_LAUNCHER, "label")
-                .text("Settings")))) {
-            mDevice.pressDPadDown();
-            count++;
-        }
-        if (!mDevice.hasObject(By.res(LEANBACK_LAUNCHER, "label").text("Settings"))) {
-            Log.d(LEANBACK_LAUNCHER, "Couldn't navigate to settings");
-        }
+        goTopRow();
+        Assert.assertNotNull("Ensure that Settings button is focused",
+            selectBidirect(By.res(TVLAUNCHER_PACKAGE, "settings").focused(true), Direction.RIGHT));
     }
 
     public void afterTestSettings(Bundle metrics) throws IOException {
@@ -101,22 +103,29 @@
     }
 
     // Measures jank while navigating to Settings from Home and back
-    @JankTest(expectedFrames=100, beforeTest="goToSettingsRow",
+    @JankTest(expectedFrames=100, beforeTest="goToSettingsButton",
             afterTest="afterTestSettings")
     @GfxMonitor(processName=SETTINGS_PACKAGE)
     public void testNavigateToSettings() throws UiObjectNotFoundException {
         for (int i = 0; i < INNER_LOOP * 10; i++) {
             // Press DPad center button to navigate to settings.
-            mDevice.pressDPadCenter();
+            mDPadUtil.pressDPadCenter();
+            mDevice.wait(Until.hasObject(
+                    By.res(SETTINGS_PACKAGE, "settings_preference_fragment_container")),
+                    SHORT_TIMEOUT);
             // Press Back button to go back to the Home screen with focus on Settings
-            mDevice.pressBack();
+            mDPadUtil.pressBack();
         }
     }
 
     // Navigates to the Settings Screen
     public void goToSettings() {
-        goToSettingsRow();
-        mDevice.pressDPadCenter();
+        goToSettingsButton();
+        mDPadUtil.pressDPadCenter();
+        Assert.assertNotNull("Ensure that Settings is being displayed",
+            mDevice.wait(
+                Until.hasObject(By.res(SETTINGS_PACKAGE, "settings_preference_fragment_container")),
+                SHORT_TIMEOUT));
     }
 
     // Measures jank while scrolling on the Settings screen
@@ -124,20 +133,59 @@
             afterTest="afterTestSettings")
     @GfxMonitor(processName=SETTINGS_PACKAGE)
     public void testSettingsScreenNavigation() throws UiObjectNotFoundException {
-        // Ensure that Settings screen is being displayed
-        mDevice.wait(Until.findObject(By.scrollable(true).res(SETTINGS_PACKAGE, "container_list")),
-                SHORT_TIMEOUT);
-        navigateDownAndUpCurrentScreen();
+        navigateDownAndUpCurrentScreen(INNER_LOOP_SETTINGS);
     }
 
-    public void navigateDownAndUpCurrentScreen() {
-        for (int i = 0; i < INNER_LOOP; i++) {
+    public void navigateDownAndUpCurrentScreen(int iterations) {
+        for (int i = 0; i < iterations; i++) {
             // Press DPad button down eight times in succession
-            mDevice.pressDPadDown();
+            mDPadUtil.pressDPadDown();
         }
-        for (int i = 0; i < INNER_LOOP; i++) {
+        for (int i = 0; i < iterations; i++) {
             // Press DPad button up eight times in succession.
-            mDevice.pressDPadUp();
+            mDPadUtil.pressDPadUp();
         }
     }
+
+    /**
+     * Select an UI element with given {@link BySelector}. This action keeps moving a focus
+     * in a given {@link Direction} until it finds a matched element.
+     * @param selector the search criteria to match an element
+     * @param direction the direction to find
+     * @param timeoutMs timeout in milliseconds to select
+     * @return a UiObject2 which represents the matched element
+     */
+    public UiObject2 select(BySelector selector, Direction direction, long timeoutMs) {
+        UiObject2 focus = mDevice.wait(Until.findObject(By.focused(true)), SHORT_TIMEOUT);
+        while (!mDevice.wait(Until.hasObject(selector), timeoutMs)) {
+            Log.d(TAG, String.format("select: moving a focus from %s to %s", focus, direction));
+            UiObject2 focused = focus;
+            mDPadUtil.pressDPad(direction);
+            focus = mDevice.wait(Until.findObject(By.focused(true)), SHORT_TIMEOUT);
+            // Hack: A focus might be lost in some UI. Take one more step forward.
+            if (focus == null) {
+                mDPadUtil.pressDPad(direction);
+                focus = mDevice.wait(Until.findObject(By.focused(true)), SHORT_TIMEOUT);
+            }
+            // Check if it reaches to an end where it no longer moves a focus to next element
+            if (focused.equals(focus)) {
+                Log.d(TAG, "select: not found until it reaches to an end.");
+                return null;
+            }
+        }
+        Log.i(TAG, String.format("select: %s is selected", focus));
+        return focus;
+    }
+
+    /**
+     * Select an element with a given {@link BySelector} in both given direction and reverse.
+     */
+    public UiObject2 selectBidirect(BySelector selector, Direction direction) {
+        Log.d(TAG, String.format("selectBidirect [direction]%s", direction));
+        UiObject2 object = select(selector, direction, SHORT_TIMEOUT);
+        if (object == null) {
+            object = select(selector, Direction.reverse(direction), SHORT_TIMEOUT);
+        }
+        return object;
+    }
 }
diff --git a/tests/jank/dialer/Android.mk b/tests/jank/dialer/Android.mk
index cc8ee89..3d662c4 100644
--- a/tests/jank/dialer/Android.mk
+++ b/tests/jank/dialer/Android.mk
@@ -25,4 +25,6 @@
     legacy-android-test
 LOCAK_SDK_VERSION := 22
 
+LOCAL_COMPATIBILITY_SUITE := device-tests
+
 include $(BUILD_PACKAGE)
diff --git a/tests/jank/dialer/AndroidTest.xml b/tests/jank/dialer/AndroidTest.xml
new file mode 100644
index 0000000..18b340b
--- /dev/null
+++ b/tests/jank/dialer/AndroidTest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Runs Dialer Jank Tests.">
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+        <option name="test-file-name" value="DialerJankTests.apk" />
+    </target_preparer>
+
+    <option name="test-tag" value="DialerJankTests" />
+    <test class="com.android.tradefed.testtype.InstrumentationTest" >
+        <option name="package" value="com.android.dialer.janktests" />
+        <option name="runner" value="android.test.InstrumentationTestRunner" />
+    </test>
+</configuration>
diff --git a/tests/jank/jankmicrobenchmark/Android.mk b/tests/jank/jankmicrobenchmark/Android.mk
index 9cee644..55ee778 100644
--- a/tests/jank/jankmicrobenchmark/Android.mk
+++ b/tests/jank/jankmicrobenchmark/Android.mk
@@ -22,11 +22,14 @@
 LOCAL_STATIC_JAVA_LIBRARIES := \
     ub-uiautomator \
     ub-janktesthelper \
+    launcher-helper-lib \
     junit
 
 LOCAL_JAVA_LIBRARIES := legacy-android-test
 
 LOCAL_SDK_VERSION := current
 
+LOCAL_COMPATIBILITY_SUITE := device-tests
+
 include $(BUILD_PACKAGE)
 
diff --git a/tests/jank/jankmicrobenchmark/AndroidTest.xml b/tests/jank/jankmicrobenchmark/AndroidTest.xml
new file mode 100644
index 0000000..1ecbd35
--- /dev/null
+++ b/tests/jank/jankmicrobenchmark/AndroidTest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Runs Platform Benchmark Jank Tests.">
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+        <option name="test-file-name" value="JankMicroBenchmarkTests.apk" />
+        <option name="test-file-name" value="ApiDemos.apk" />
+    </target_preparer>
+
+    <option name="test-tag" value="JankMicroBenchmarkTests" />
+    <test class="com.android.tradefed.testtype.InstrumentationTest" >
+        <option name="package" value="com.android.jankmicrobenchmark.janktests" />
+        <option name="runner" value="android.test.InstrumentationTestRunner" />
+    </test>
+</configuration>
diff --git a/tests/jank/jankmicrobenchmark/src/com/android/jankmicrobenchmark/janktests/ApiDemoJankTests.java b/tests/jank/jankmicrobenchmark/src/com/android/jankmicrobenchmark/janktests/ApiDemoJankTests.java
index 9b8a9bb..101eb40 100644
--- a/tests/jank/jankmicrobenchmark/src/com/android/jankmicrobenchmark/janktests/ApiDemoJankTests.java
+++ b/tests/jank/jankmicrobenchmark/src/com/android/jankmicrobenchmark/janktests/ApiDemoJankTests.java
@@ -23,6 +23,7 @@
 import android.support.test.jank.GfxMonitor;
 import android.support.test.jank.JankTest;
 import android.support.test.jank.JankTestBase;
+import android.support.test.launcherhelper.LauncherStrategyFactory;
 import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.Direction;
 import android.support.test.uiautomator.UiDevice;
@@ -54,6 +55,7 @@
         super.setUp();
         mDevice = UiDevice.getInstance(getInstrumentation());
         mDevice.setOrientationNatural();
+        LauncherStrategyFactory.getInstance(mDevice).getLauncherStrategy().open();
     }
 
     @Override
diff --git a/tests/jank/notificationsgenerator_wear/Android.mk b/tests/jank/notificationsgenerator_wear/Android.mk
deleted file mode 100644
index 5352278..0000000
--- a/tests/jank/notificationsgenerator_wear/Android.mk
+++ /dev/null
@@ -1,20 +0,0 @@
-LOCAL_PATH := $(call my-dir)
-include $(CLEAR_VARS)
-
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
-
-LOCAL_SDK_VERSION := current
-LOCAL_DEX_PREOPT := false
-
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
-
-LOCAL_PACKAGE_NAME := NotificationsGeneratorWear
-
-LOCAL_STATIC_JAVA_LIBRARIES := \
-    android-support-v4
-
-LOCAL_CERTIFICATE := vendor/unbundled_google/libraries/certs/clockwork
-
-LOCAL_PROGUARD_FLAG_FILES := proguard.flags
-
-include $(BUILD_PACKAGE)
\ No newline at end of file
diff --git a/tests/jank/notificationsgenerator_wear/AndroidManifest.xml b/tests/jank/notificationsgenerator_wear/AndroidManifest.xml
deleted file mode 100644
index 6e3ec81..0000000
--- a/tests/jank/notificationsgenerator_wear/AndroidManifest.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.google.android.wearable.support" >
-
-     <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="23" />
-
-    <uses-permission android:name="android.permission.INTERNET" />
-
-    <application
-        android:allowBackup="true"
-        android:icon="@drawable/ic_launcher"
-        android:label="@string/app_name"
-        android:theme="@android:style/Theme.DeviceDefault" >
-        <activity
-            android:name="com.google.android.wearable.support.CustomNotificationStubBroadcastActivity"
-            android:label="@string/app_name"
-            android:launchMode="singleTask" >
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-
-                <category android:name="android.intent.category.LAUNCHER" />
-            </intent-filter>
-        </activity>
-        <activity
-            android:name="com.google.android.wearable.support.CustomNotificationRemoteInputActivity"
-            android:allowEmbedded="true"
-            android:exported="true"
-            android:taskAffinity=""
-            android:theme="@android:style/Theme.DeviceDefault.Light" >
-        </activity>
-        <receiver
-            android:name="com.google.android.wearable.support.CustomPostNotificationReceiver"
-            android:exported="true" >
-            <intent-filter>
-                <action android:name="com.google.android.wearable.support.wearnotificationgenerator.SHOW_NOTIFICATION" />
-            </intent-filter>
-        </receiver>
-    </application>
-
-</manifest>
\ No newline at end of file
diff --git a/tests/jank/notificationsgenerator_wear/proguard.flags b/tests/jank/notificationsgenerator_wear/proguard.flags
deleted file mode 100644
index 872c809..0000000
--- a/tests/jank/notificationsgenerator_wear/proguard.flags
+++ /dev/null
@@ -1,54 +0,0 @@
-# To enable ProGuard in your project, edit project.properties
-# to define the proguard.config property as described in that file.
-#
-# Add project specific ProGuard rules here.
-# By default, the flags in this file are appended to flags specified
-# in ${sdk.dir}/tools/proguard/proguard-android.txt
-# You can edit the include path and order by changing the ProGuard
-# include property in project.properties.
-#
-# For more details, see
-#   http://developer.android.com/guide/developing/tools/proguard.html
-
-# Add any project specific keep options here:
-
-# If your project uses WebView with JS, uncomment the following
-# and specify the fully qualified class name to the JavaScript interface
-# class:
-#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
-#   public *;
-#}
-
-# GmsCore Proguard rules.
-# See: https://developer.android.com/google/play-services/setup.html
--keep class * extends java.util.ListResourceBundle {
-    protected Object[][] getContents();
-}
-
-# Keep SafeParcelable value, needed for reflection. This is required to support backwards
-# compatibility of some classes.
--keep public class com.google.android.gms.common.internal.safeparcel.SafeParcelable {
-    public static final *** NULL;
-}
-
-# Keep the names of classes/members we need for client functionality.
--keepnames @com.google.android.gms.common.annotation.KeepName class *
--keepclassmembernames class * {
-    @com.google.android.gms.common.annotation.KeepName *;
-}
-
-# Needed for Parcelable/SafeParcelable Creators to not get stripped
--keepnames class * implements android.os.Parcelable {
-    public static final ** CREATOR;
-}
-
-# Suppress reblochon client library unexpected warnings
-# TODO: Remove the following suppression lines once either the platform prebuilts get updated
-# or the reblochon client library removes unexpected dependencies.
-# The following warnings were encountered:
-#   com.google.android.gms.car.* can't find android.view.SearchEvent
-#   com.google.android.gms.cast.* can't find android.support.v7.media.*
--dontwarn android.view.SearchEvent
--dontwarn android.support.v7.media.**
-
-# End GmsCore Proguard rules.
\ No newline at end of file
diff --git a/tests/jank/notificationsgenerator_wear/res/drawable-hdpi/ic_launcher.png b/tests/jank/notificationsgenerator_wear/res/drawable-hdpi/ic_launcher.png
deleted file mode 100644
index 6db17aa..0000000
--- a/tests/jank/notificationsgenerator_wear/res/drawable-hdpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/tests/jank/notificationsgenerator_wear/res/drawable-hdpi/ic_notification.png b/tests/jank/notificationsgenerator_wear/res/drawable-hdpi/ic_notification.png
deleted file mode 100644
index 0401013..0000000
--- a/tests/jank/notificationsgenerator_wear/res/drawable-hdpi/ic_notification.png
+++ /dev/null
Binary files differ
diff --git a/tests/jank/notificationsgenerator_wear/res/drawable-hdpi/ic_reply.png b/tests/jank/notificationsgenerator_wear/res/drawable-hdpi/ic_reply.png
deleted file mode 100644
index b3e6146..0000000
--- a/tests/jank/notificationsgenerator_wear/res/drawable-hdpi/ic_reply.png
+++ /dev/null
Binary files differ
diff --git a/tests/jank/notificationsgenerator_wear/res/drawable-mdpi/ic_launcher.png b/tests/jank/notificationsgenerator_wear/res/drawable-mdpi/ic_launcher.png
deleted file mode 100644
index ddedab6..0000000
--- a/tests/jank/notificationsgenerator_wear/res/drawable-mdpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/tests/jank/notificationsgenerator_wear/res/drawable-mdpi/ic_notification.png b/tests/jank/notificationsgenerator_wear/res/drawable-mdpi/ic_notification.png
deleted file mode 100644
index 7e93c94..0000000
--- a/tests/jank/notificationsgenerator_wear/res/drawable-mdpi/ic_notification.png
+++ /dev/null
Binary files differ
diff --git a/tests/jank/notificationsgenerator_wear/res/drawable-mdpi/ic_reply.png b/tests/jank/notificationsgenerator_wear/res/drawable-mdpi/ic_reply.png
deleted file mode 100644
index 8b01970..0000000
--- a/tests/jank/notificationsgenerator_wear/res/drawable-mdpi/ic_reply.png
+++ /dev/null
Binary files differ
diff --git a/tests/jank/notificationsgenerator_wear/res/drawable-xhdpi/ic_launcher.png b/tests/jank/notificationsgenerator_wear/res/drawable-xhdpi/ic_launcher.png
deleted file mode 100644
index 36d56a4..0000000
--- a/tests/jank/notificationsgenerator_wear/res/drawable-xhdpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/tests/jank/notificationsgenerator_wear/res/drawable-xhdpi/ic_notification.png b/tests/jank/notificationsgenerator_wear/res/drawable-xhdpi/ic_notification.png
deleted file mode 100644
index 1bda506..0000000
--- a/tests/jank/notificationsgenerator_wear/res/drawable-xhdpi/ic_notification.png
+++ /dev/null
Binary files differ
diff --git a/tests/jank/notificationsgenerator_wear/res/drawable-xhdpi/ic_reply.png b/tests/jank/notificationsgenerator_wear/res/drawable-xhdpi/ic_reply.png
deleted file mode 100644
index 784c4ba..0000000
--- a/tests/jank/notificationsgenerator_wear/res/drawable-xhdpi/ic_reply.png
+++ /dev/null
Binary files differ
diff --git a/tests/jank/notificationsgenerator_wear/res/drawable-xxhdpi/ic_launcher.png b/tests/jank/notificationsgenerator_wear/res/drawable-xxhdpi/ic_launcher.png
deleted file mode 100644
index cabc1c9..0000000
--- a/tests/jank/notificationsgenerator_wear/res/drawable-xxhdpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/tests/jank/notificationsgenerator_wear/res/drawable-xxhdpi/ic_notification.png b/tests/jank/notificationsgenerator_wear/res/drawable-xxhdpi/ic_notification.png
deleted file mode 100644
index eb0684b..0000000
--- a/tests/jank/notificationsgenerator_wear/res/drawable-xxhdpi/ic_notification.png
+++ /dev/null
Binary files differ
diff --git a/tests/jank/notificationsgenerator_wear/res/drawable-xxhdpi/ic_reply.png b/tests/jank/notificationsgenerator_wear/res/drawable-xxhdpi/ic_reply.png
deleted file mode 100644
index ffc524a..0000000
--- a/tests/jank/notificationsgenerator_wear/res/drawable-xxhdpi/ic_reply.png
+++ /dev/null
Binary files differ
diff --git a/tests/jank/notificationsgenerator_wear/res/drawable-xxxhdpi/ic_launcher.png b/tests/jank/notificationsgenerator_wear/res/drawable-xxxhdpi/ic_launcher.png
deleted file mode 100644
index ce22034..0000000
--- a/tests/jank/notificationsgenerator_wear/res/drawable-xxxhdpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/tests/jank/notificationsgenerator_wear/res/values/strings.xml b/tests/jank/notificationsgenerator_wear/res/values/strings.xml
deleted file mode 100644
index b363872..0000000
--- a/tests/jank/notificationsgenerator_wear/res/values/strings.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="app_name">NotificationsGeneratorWear</string>
-
-    <string name="notification_content">Notification Content</string>
-    <string name="notification_title">Clockwork Notifications</string>
-    <string name="notification_posted">Notification posted</string>
-</resources>
\ No newline at end of file
diff --git a/tests/jank/notificationsgenerator_wear/res/values/styles.xml b/tests/jank/notificationsgenerator_wear/res/values/styles.xml
deleted file mode 100644
index e6cd596..0000000
--- a/tests/jank/notificationsgenerator_wear/res/values/styles.xml
+++ /dev/null
@@ -1,16 +0,0 @@
-<resources>
-
-    <!--
-        Base application theme, dependent on API level. This theme is replaced
-        by AppBaseTheme from res/values-vXX/styles.xml on newer devices.
-    -->
-    <style name="AppBaseTheme" parent="android:Theme.Holo.Light.DarkActionBar">
-        <item name="android:windowAnimationStyle">@null</item>
-    </style>
-
-    <!-- Application theme. -->
-    <style name="AppTheme" parent="AppBaseTheme">
-        <!-- All customizations that are NOT specific to a particular API-level can go here. -->
-    </style>
-
-</resources>
\ No newline at end of file
diff --git a/tests/jank/notificationsgenerator_wear/src/com/google/android/wearable/support/CustomNotificationStubBroadcastActivity.java b/tests/jank/notificationsgenerator_wear/src/com/google/android/wearable/support/CustomNotificationStubBroadcastActivity.java
deleted file mode 100644
index 2ca29be..0000000
--- a/tests/jank/notificationsgenerator_wear/src/com/google/android/wearable/support/CustomNotificationStubBroadcastActivity.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
-
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.google.android.wearable.support;
-
-import android.app.Activity;
-import android.content.Intent;
-import android.os.Bundle;
-
-/**
- * A simple activity which broadcasts to our receiver and exits.
- */
-public class CustomNotificationStubBroadcastActivity extends Activity {
-    public static final String LOAD_CARDS_FOR_TEST_ACTION =
-            "com.google.android.wearable.support.wearnotificationgenerator.SHOW_NOTIFICATION";
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        Intent intent = new Intent();
-        intent.setAction(LOAD_CARDS_FOR_TEST_ACTION);
-        sendBroadcast(intent);
-        finish();
-    }
-}
\ No newline at end of file
diff --git a/tests/jank/notificationsgenerator_wear/src/com/google/android/wearable/support/CustomPostNotificationReceiver.java b/tests/jank/notificationsgenerator_wear/src/com/google/android/wearable/support/CustomPostNotificationReceiver.java
deleted file mode 100644
index 0224dc5..0000000
--- a/tests/jank/notificationsgenerator_wear/src/com/google/android/wearable/support/CustomPostNotificationReceiver.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
-
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.google.android.wearable.support;
-
-import android.app.PendingIntent;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.support.v4.app.NotificationCompat;
-import android.support.v4.app.NotificationCompat.WearableExtender;
-import android.support.v4.app.NotificationManagerCompat;
-import android.support.v4.app.RemoteInput;
-import android.widget.Toast;
-
-
-/**
- * Receiver to generate notification cards.
- */
-public class CustomPostNotificationReceiver extends BroadcastReceiver {
-
-    public static final String REMOTE_INPUT_KEY = "demoCardQuickReply";
-    private static final int NOTIFICATION_CARDS_COUNT = 10;
-
-    public CustomPostNotificationReceiver() {
-    }
-
-    @Override
-    public void onReceive(Context context, Intent intent) {
-        String contentTitle = context.getResources().getString(R.string.notification_title);
-        // Get an instance of NotificationManager service
-        NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
-        for (int i = 1; i <= NOTIFICATION_CARDS_COUNT; i++) {
-            Intent remoteInputIntent = new Intent(context,
-                    CustomNotificationRemoteInputActivity.class);
-            PendingIntent remoteInputPendingIntent = PendingIntent
-                    .getActivity(context, i + NOTIFICATION_CARDS_COUNT, remoteInputIntent,
-                            PendingIntent.FLAG_UPDATE_CURRENT);
-            RemoteInput remoteInput = new RemoteInput.Builder(REMOTE_INPUT_KEY)
-                    .setLabel("Quick reply")
-                    .build();
-
-            NotificationCompat.Action action = new NotificationCompat.Action.Builder(
-                    R.drawable.ic_reply, "Reply", remoteInputPendingIntent)
-                    .addRemoteInput(remoteInput)
-                    .build();
-
-            NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(context)
-                    .setSmallIcon(R.drawable.ic_notification)
-                    .setContentTitle(contentTitle + "-" + i)
-                    .setCategory(NotificationCompat.CATEGORY_MESSAGE)
-                    .setContentText(String.format("From iteration %d", i))
-                    .extend(new WearableExtender()
-                            .addAction(action));
-
-            // Build the notification and issues it with notification manager
-            notificationManager.notify(i, notificationBuilder.build());
-        }
-
-        // Show a toast once notifications cards are posted
-        Toast.makeText(context, context.getString(R.string.notification_posted), Toast.LENGTH_SHORT)
-                .show();
-    }
-}
\ No newline at end of file
diff --git a/tests/jank/sysapp_wear/Android.mk b/tests/jank/sysapp_wear/Android.mk
index 208cfad..c47be20 100644
--- a/tests/jank/sysapp_wear/Android.mk
+++ b/tests/jank/sysapp_wear/Android.mk
@@ -28,4 +28,6 @@
 
 LOCAL_SDK_VERSION := current
 
+LOCAL_COMPATIBILITY_SUITE := device-tests
+
 include $(BUILD_PACKAGE)
diff --git a/tests/jank/sysapp_wear/AndroidTest.xml b/tests/jank/sysapp_wear/AndroidTest.xml
new file mode 100644
index 0000000..2ca3ab4
--- /dev/null
+++ b/tests/jank/sysapp_wear/AndroidTest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Runs Wearable Platform System Apps Jank Tests.">
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+        <option name="test-file-name" value="SysAppJankTestsWear.apk" />
+        <option name="test-file-name" value="NotificationsGeneratorWear.apk" />
+    </target_preparer>
+    <target_preparer class="com.android.tradefed.targetprep.InstallApkSetup">
+        <option name="post-install-cmd" value="am start -n com.google.android.wearable.support/.CustomNotificationStubBroadcastActivity" />
+    </target_preparer>
+
+    <option name="post-boot-command" value="am broadcast -a com.google.android.clockwork.action.TEST_MODE" />
+    <option name="post-boot-command" value="setprop debug.hwui.filter_test_overhead true" />
+    <option name="post-boot-command" value="settings put secure accessibility_disable_animations 0" />
+
+    <option name="test-tag" value="SysAppJankTestsWear" />
+    <test class="com.android.tradefed.testtype.InstrumentationTest">
+        <option name="package" value="com.android.wearable.sysapp.janktests" />
+        <option name="runner" value="android.test.InstrumentationTestRunner" />
+    </test>
+</configuration>
diff --git a/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/CardsJankTest.java b/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/CardsJankTest.java
index 5282f29..f2df0a2 100644
--- a/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/CardsJankTest.java
+++ b/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/CardsJankTest.java
@@ -64,7 +64,6 @@
     public void openSwipeCard() throws Exception {
         mHelper.hasDemoCards();
         mHelper.swipeUp();
-        mHelper.swipeUp();
     }
 
     // Measure jank when dismissing a card
diff --git a/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/SysAppTestHelper.java b/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/SysAppTestHelper.java
index e78204e..4ed3afd 100644
--- a/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/SysAppTestHelper.java
+++ b/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/SysAppTestHelper.java
@@ -22,13 +22,10 @@
 import android.os.SystemClock;
 import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.UiDevice;
-import android.support.test.uiautomator.UiObject;
 import android.support.test.uiautomator.UiObject2;
-import android.support.test.uiautomator.UiSelector;
 import android.support.test.uiautomator.Until;
 import android.util.Log;
 import android.view.KeyEvent;
-
 import junit.framework.Assert;
 
 /**
@@ -46,31 +43,15 @@
     public static final int SHORT_TIMEOUT = 500;
     public static final int FLING_SPEED = 5000;
     private static final String LOG_TAG = SysAppTestHelper.class.getSimpleName();
-    private static final long NEW_CARD_TIMEOUT_MS = 5 * 1000; // 5s
     private static final String RELOAD_NOTIFICATION_CARD_INTENT = "com.google.android.wearable."
             + "support.wearnotificationgenerator.SHOW_NOTIFICATION";
     private static final String HOME_INDICATOR = "charging_icon";
+    private static final String NO_NOTIFICATION_ID = "no_notifications";
+    private static final String STREAM_CARD_ID = "stream_card";
 
-    // Demo card selectors
-    private static final UiSelector CARD_SELECTOR = new UiSelector()
-            .resourceId("com.google.android.wearable.app:id/snippet");
-    private static final UiSelector TITLE_SELECTOR = new UiSelector()
-            .resourceId("com.google.android.wearable.app:id/title");
-    private static final UiSelector CLOCK_SELECTOR = new UiSelector()
-            .resourceId("com.google.android.wearable.app:id/clock_bar");
-    private static final UiSelector ICON_SELECTOR = new UiSelector()
-            .resourceId("com.google.android.wearable.app:id/icon");
-    private static final UiSelector TEXT_SELECTOR = new UiSelector()
-            .resourceId("com.google.android.wearable.app:id/text");
-    private static final UiSelector STATUS_BAR_SELECTOR = new UiSelector()
-            .resourceId("com.google.android.wearable.app:id/status_bar_icons");
     private static SysAppTestHelper sysAppTestHelperInstance;
     private UiDevice mDevice = null;
     private Instrumentation instrumentation = null;
-    private UiObject mCard = null;
-    private UiObject mTitle = null;
-    private UiObject mIcon = null;
-    private UiObject mText = null;
     private Intent mIntent = null;
 
     /**
@@ -82,10 +63,6 @@
         this.mDevice = mDevice;
         this.instrumentation = instrumentation;
         mIntent = new Intent();
-        mCard = mDevice.findObject(CARD_SELECTOR);
-        mTitle = mDevice.findObject(TITLE_SELECTOR);
-        mIcon = mDevice.findObject(ICON_SELECTOR);
-        mText = mDevice.findObject(TEXT_SELECTOR);
     }
 
     public static SysAppTestHelper getInstance(UiDevice device, Instrumentation instrumentation) {
@@ -173,34 +150,23 @@
     public void hasDemoCards() {
         // Device should be pre-loaded with demo cards.
 
-        goBackHome(); // Start by going to Home.
+        goBackHome();
 
-        if (!mTitle.waitForExists(NEW_CARD_TIMEOUT_MS)) {
-            Log.d(LOG_TAG, "Card previews not available, swiping up");
-            swipeUp();
-            // For few devices, demo card preview is hidden by default. So swipe once to bring up
-            // the card.
-        }
+        // Swipe up to go to notification tray.
+        swipeUp();
 
-        // First card from the pre-loaded demo cards could be either in peek view
-        // or in full view(e.g Dory) or no peek view(Sturgeon). Ensure to check for demo cards
-        // existence in both cases.
-        if (!(mCard.waitForExists(NEW_CARD_TIMEOUT_MS)
-                || mTitle.waitForExists(NEW_CARD_TIMEOUT_MS)
-                || mIcon.waitForExists(NEW_CARD_TIMEOUT_MS)
-                || mText.waitForExists(NEW_CARD_TIMEOUT_MS))) {
-            Log.d(LOG_TAG, "Demo cards not found, going to reload the cards");
+        if (waitForSysAppUiObject2(NO_NOTIFICATION_ID) != null) {
+            Log.d(LOG_TAG, "No cards, going to reload the cards");
             // If there are no Demo cards, reload them.
+            goBackHome();
             reloadDemoCards();
-            if (!mTitle.waitForExists(NEW_CARD_TIMEOUT_MS)) {
-                swipeUp(); // For few devices, demo card preview is hidden by
-                // default. So swipe once to bring up the card.
-            }
         }
-        Assert.assertTrue("no cards available for testing",
-                (mTitle.waitForExists(NEW_CARD_TIMEOUT_MS)
-                        || mIcon.waitForExists(NEW_CARD_TIMEOUT_MS)
-                        || mText.waitForExists(NEW_CARD_TIMEOUT_MS)));
+        else if (waitForSysAppUiObject2(STREAM_CARD_ID) != null){
+            goBackHome();
+        }
+        else {
+            Assert.fail("Swipe up failed to go to notification tray.");
+        }
     }
 
     // This will ensure to reload notification cards by launching NotificationsGeneratorWear app
diff --git a/tests/jank/touch_latency_wear/Android.mk b/tests/jank/touch_latency_wear/Android.mk
index 88edc57..56afd10 100644
--- a/tests/jank/touch_latency_wear/Android.mk
+++ b/tests/jank/touch_latency_wear/Android.mk
@@ -28,4 +28,6 @@
 
 LOCAL_SDK_VERSION := current
 
+LOCAL_COMPATIBILITY_SUITE := device-tests
+
 include $(BUILD_PACKAGE)
diff --git a/tests/jank/touch_latency_wear/AndroidTest.xml b/tests/jank/touch_latency_wear/AndroidTest.xml
new file mode 100644
index 0000000..761c0d5
--- /dev/null
+++ b/tests/jank/touch_latency_wear/AndroidTest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Runs Wearable Platform System Apps Jank Tests.">
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+        <option name="test-file-name" value="TouchLatencyJankTestWear.apk" />
+        <option name="test-file-name" value="TouchLatency.apk" />
+    </target_preparer>
+
+    <option name="post-boot-command" value="am broadcast -a com.google.android.clockwork.action.TEST_MODE" />
+    <option name="post-boot-command" value="setprop debug.hwui.filter_test_overhead true" />
+    <option name="post-boot-command" value="settings put secure accessibility_disable_animations 0" />
+
+    <option name="test-tag" value="BouncingBallJankTestsWear" />
+    <test class="com.android.tradefed.testtype.InstrumentationTest">
+        <option name="package" value="com.android.wearable.touch.janktests" />
+        <option name="runner" value="android.test.InstrumentationTestRunner" />
+    </test>
+</configuration>
diff --git a/tests/jank/uibench/Android.mk b/tests/jank/uibench/Android.mk
index 9096784..14b5749 100644
--- a/tests/jank/uibench/Android.mk
+++ b/tests/jank/uibench/Android.mk
@@ -28,4 +28,6 @@
 
 LOCAL_SDK_VERSION := current
 
+LOCAL_COMPATIBILITY_SUITE := device-tests
+
 include $(BUILD_PACKAGE)
diff --git a/tests/jank/uibench/AndroidTest.xml b/tests/jank/uibench/AndroidTest.xml
new file mode 100644
index 0000000..df06786
--- /dev/null
+++ b/tests/jank/uibench/AndroidTest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Runs Platform UiBench Jank Tests.">
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+        <option name="test-file-name" value="UiBenchJankTests.apk" />
+        <option name="test-file-name" value="UiBench.apk" />
+    </target_preparer>
+
+    <option name="post-boot-command" value="am broadcast -a com.google.android.clockwork.action.TEST_MODE" />
+    <option name="post-boot-command" value="setprop debug.hwui.filter_test_overhead true" />
+    <option name="post-boot-command" value="settings put secure accessibility_disable_animations 0" />
+
+    <option name="test-tag" value="UiBenchJankTests" />
+    <test class="com.android.tradefed.testtype.InstrumentationTest">
+        <option name="package" value="com.android.uibench.janktests" />
+        <option name="runner" value="android.test.InstrumentationTestRunner" />
+    </test>
+</configuration>
diff --git a/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchJankTests.java b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchJankTests.java
index e258c13..989023d 100644
--- a/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchJankTests.java
+++ b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchJankTests.java
@@ -18,6 +18,7 @@
 
 import static com.android.uibench.janktests.UiBenchJankTestsHelper.EXPECTED_FRAMES;
 import static com.android.uibench.janktests.UiBenchJankTestsHelper.PACKAGE_NAME;
+import static com.android.uibench.janktests.UiBenchJankTestsHelper.SHORT_EXPECTED_FRAMES;
 
 import android.os.SystemClock;
 import android.support.test.jank.GfxMonitor;
@@ -27,6 +28,7 @@
 import android.support.test.uiautomator.UiDevice;
 import android.support.test.uiautomator.Until;
 import android.widget.ListView;
+
 import junit.framework.Assert;
 
 /**
@@ -53,121 +55,193 @@
         super.tearDown();
     }
 
-    // Open dialog list from General
     public void openDialogList() {
         mHelper.launchActivity("DialogListActivity", "Dialog");
         mHelper.mContents = mDevice.wait(Until.findObject(
-                By.clazz(ListView.class)), mHelper.TIMEOUT);
+                By.clazz(ListView.class)), UiBenchJankTestsHelper.TIMEOUT);
         Assert.assertNotNull("Dialog List View isn't found", mHelper.mContents);
     }
 
-    // Test dialoglist fling
     @JankTest(beforeTest = "openDialogList", expectedFrames = EXPECTED_FRAMES)
     @GfxMonitor(processName = PACKAGE_NAME)
     public void testDialogListFling() {
-        mHelper.flingUpDown(mHelper.mContents, mHelper.SHORT_TIMEOUT, 1);
+        mHelper.flingUpDown(mHelper.mContents, 1);
     }
 
-    // Open Fullscreen Overdraw from General
     public void openFullscreenOverdraw() {
         mHelper.launchActivity("FullscreenOverdrawActivity",
                 "General/Fullscreen Overdraw");
     }
 
-    // Measure fullscreen overdraw jank
     @JankTest(beforeTest = "openFullscreenOverdraw", expectedFrames = EXPECTED_FRAMES)
     @GfxMonitor(processName = PACKAGE_NAME)
     public void testFullscreenOverdraw() {
-        SystemClock.sleep(mHelper.LONG_TIMEOUT * 5);
+        SystemClock.sleep(UiBenchJankTestsHelper.FULL_TEST_DURATION);
     }
 
-    // Open GL TextureView from General
     public void openGLTextureView() {
         mHelper.launchActivity("GlTextureViewActivity",
                 "General/GL TextureView");
     }
 
-    // Measure GL TextureView jank metrics
     @JankTest(beforeTest = "openGLTextureView", expectedFrames = EXPECTED_FRAMES)
     @GfxMonitor(processName = PACKAGE_NAME)
     public void testGLTextureView() {
-        SystemClock.sleep(mHelper.LONG_TIMEOUT * 5);
+        SystemClock.sleep(UiBenchJankTestsHelper.FULL_TEST_DURATION);
     }
 
-    // Open Invalidate from General
     public void openInvalidate() {
         mHelper.launchActivity("InvalidateActivity",
                 "General/Invalidate");
     }
 
-    // Measure Invalidate jank metrics
     @JankTest(beforeTest = "openInvalidate", expectedFrames = EXPECTED_FRAMES)
     @GfxMonitor(processName = PACKAGE_NAME)
     public void testInvalidate() {
-        SystemClock.sleep(mHelper.LONG_TIMEOUT * 5);
+        SystemClock.sleep(UiBenchJankTestsHelper.FULL_TEST_DURATION);
     }
 
-    // Open Trivial Animation from General
+    public void openInvalidateTree() {
+        mHelper.launchActivity("InvalidateTreeActivity",
+                "General/Invalidate Tree");
+    }
+
+    @JankTest(beforeTest = "openInvalidateTree", expectedFrames = EXPECTED_FRAMES)
+    @GfxMonitor(processName = PACKAGE_NAME)
+    public void testInvalidateTree() {
+        SystemClock.sleep(UiBenchJankTestsHelper.FULL_TEST_DURATION);
+    }
+
     public void openTrivialAnimation() {
         mHelper.launchActivity("TrivialAnimationActivity",
                 "General/Trivial Animation");
     }
 
-    // Measure TrivialAnimation jank metrics
     @JankTest(beforeTest = "openTrivialAnimation", expectedFrames = EXPECTED_FRAMES)
     @GfxMonitor(processName = PACKAGE_NAME)
     public void testTrivialAnimation() {
-        SystemClock.sleep(mHelper.LONG_TIMEOUT * 5);
+        SystemClock.sleep(UiBenchJankTestsHelper.FULL_TEST_DURATION);
     }
 
-    // Open Trivial listview from General
     public void openTrivialListView() {
         mHelper.launchActivityAndAssert("TrivialListActivity", "General/Trivial ListView");
     }
 
-    // Test trivialListView fling
     @JankTest(beforeTest = "openTrivialListView", expectedFrames = EXPECTED_FRAMES)
     @GfxMonitor(processName = PACKAGE_NAME)
     public void testTrivialListViewFling() {
-        mHelper.flingUpDown(mHelper.mContents, mHelper.SHORT_TIMEOUT, 2);
+        mHelper.flingUpDown(mHelper.mContents, 2);
     }
 
-    // Open Trivial RecyclerView from General
+    public void openFadingEdgeListView() {
+        mHelper.launchActivityAndAssert("FadingEdgeListActivity", "General/Fading Edge ListView");
+    }
+
+    @JankTest(beforeTest = "openFadingEdgeListView", expectedFrames = EXPECTED_FRAMES)
+    @GfxMonitor(processName = PACKAGE_NAME)
+    public void testFadingEdgeListViewFling() {
+        mHelper.flingUpDown(mHelper.mContents, 2);
+    }
+
+    public void openSaveLayerInterleaveActivity() {
+        mHelper.launchActivityAndAssert("SaveLayerInterleaveActivity", "General/SaveLayer Animation");
+    }
+
+    @JankTest(beforeTest = "openSaveLayerInterleaveActivity", expectedFrames = EXPECTED_FRAMES)
+    @GfxMonitor(processName = PACKAGE_NAME)
+    public void testSaveLayerAnimation() {
+        SystemClock.sleep(UiBenchJankTestsHelper.FULL_TEST_DURATION);
+    }
+
     public void openTrivialRecyclerView() {
         mHelper.launchActivityAndAssert("TrivialRecyclerViewActivity",
                 "General/Trivial RecyclerView");
     }
 
-    // Test trivialRecyclerView fling
     @JankTest(beforeTest = "openTrivialRecyclerView", expectedFrames = EXPECTED_FRAMES)
     @GfxMonitor(processName = PACKAGE_NAME)
     public void testTrivialRecyclerListViewFling() {
-        mHelper.flingUpDown(mHelper.mContents, mHelper.SHORT_TIMEOUT, 2);
+        mHelper.flingUpDown(mHelper.mContents, 2);
     }
 
-    // Open Slow Bind RecyclerView from General
     public void openSlowBindRecyclerView() {
         mHelper.launchActivityAndAssert("SlowBindRecyclerViewActivity",
                 "General/Slow Bind RecyclerView");
     }
 
-    // Test trivialRecyclerView fling
     @JankTest(beforeTest = "openSlowBindRecyclerView", expectedFrames = EXPECTED_FRAMES)
     @GfxMonitor(processName = PACKAGE_NAME)
     public void testSlowBindRecyclerViewFling() {
-        mHelper.flingUpDown(mHelper.mContents, mHelper.SHORT_TIMEOUT, 2);
+        mHelper.flingUpDown(mHelper.mContents, 2);
     }
 
-    // Open Inflation Listview contents
+    public void openSlowNestedRecyclerView() {
+        mHelper.launchActivityAndAssert("SlowNestedRecyclerViewActivity",
+                "General/Slow Nested RecyclerView");
+    }
+
+    @JankTest(beforeTest = "openSlowNestedRecyclerView", expectedFrames = EXPECTED_FRAMES)
+    @GfxMonitor(processName = PACKAGE_NAME)
+    public void testSlowNestedRecyclerViewFling() {
+        mHelper.flingUpDown(mHelper.mContents, 2);
+    }
+
+    @JankTest(/* NOTE: relaunch between loops */ beforeLoop = "openSlowNestedRecyclerView",
+            expectedFrames = SHORT_EXPECTED_FRAMES)
+    @GfxMonitor(processName = PACKAGE_NAME)
+    public void testSlowNestedRecyclerViewInitialFling() {
+        mHelper.slowSingleFlingDown(mHelper.mContents);
+    }
+
     public void openInflatingListView() {
         mHelper.launchActivityAndAssert("InflatingListActivity",
                 "Inflation/Inflating ListView");
     }
 
-    // Test Inflating List View fling
     @JankTest(beforeTest = "openInflatingListView", expectedFrames = EXPECTED_FRAMES)
     @GfxMonitor(processName = PACKAGE_NAME)
     public void testInflatingListViewFling() {
-        mHelper.flingUpDown(mHelper.mContents, mHelper.SHORT_TIMEOUT, 2);
+        mHelper.flingUpDown(mHelper.mContents, 2);
+    }
+
+    public void openNavigationDrawerActivity() {
+        mHelper.launchActivityAndAssert("NavigationDrawerActivity", "Navigation Drawer Activity");
+        mHelper.mContents.setGestureMargins(0, 0, 10, 0);
+    }
+
+    @JankTest(beforeTest = "openNavigationDrawerActivity", expectedFrames = EXPECTED_FRAMES)
+    @GfxMonitor(processName = PACKAGE_NAME)
+    public void testOpenNavigationDrawer() {
+        mHelper.swipeRightLeft(mHelper.mContents, 4);
+    }
+
+    public void openNotificationShade() {
+        mHelper.launchActivityAndAssert("NotificationShadeActivity", "Notification Shade");
+    }
+
+    @JankTest(beforeTest = "openNotificationShade", expectedFrames = EXPECTED_FRAMES)
+    @GfxMonitor(processName = PACKAGE_NAME)
+    public void testOpenNotificationShade() {
+        mHelper.flingUpDown(mHelper.mContents, 2, true);
+    }
+
+    public void openResizeHWLayer() {
+        mHelper.launchActivity("ResizeHWLayerActivity", "General/Resize HW Layer");
+    }
+
+    @JankTest(beforeTest = "openResizeHWLayer", expectedFrames = EXPECTED_FRAMES)
+    @GfxMonitor(processName = PACKAGE_NAME)
+    public void testResizeHWLayer() {
+        SystemClock.sleep(UiBenchJankTestsHelper.FULL_TEST_DURATION);
+    }
+
+    public void openClippedListView() {
+        mHelper.launchActivityAndAssert("ClippedListActivity", "General/Clipped ListView");
+    }
+
+    @JankTest(beforeTest = "openClippedListView", expectedFrames = EXPECTED_FRAMES)
+    @GfxMonitor(processName = PACKAGE_NAME)
+    public void testClippedListView() {
+        mHelper.swipeRightLeft(mHelper.mContents, 4);
     }
 }
diff --git a/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchJankTestsHelper.java b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchJankTestsHelper.java
index a5e2af4..c2e17bd 100644
--- a/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchJankTestsHelper.java
+++ b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchJankTestsHelper.java
@@ -25,40 +25,51 @@
 import android.support.test.uiautomator.UiDevice;
 import android.support.test.uiautomator.UiObject2;
 import android.support.test.uiautomator.Until;
+import android.util.DisplayMetrics;
+
 import junit.framework.Assert;
+
 /**
  * Jank benchmark tests helper for UiBench app
  */
-
 public class UiBenchJankTestsHelper {
     public static final int LONG_TIMEOUT = 5000;
+    public static final int FULL_TEST_DURATION = 25000;
     public static final int TIMEOUT = 250;
     public static final int SHORT_TIMEOUT = 2000;
     public static final int EXPECTED_FRAMES = 100;
 
+    /**
+     * Only to be used for initial-fling tests, or similar cases
+     * where perf during brief experience is important.
+     */
+    public static final int SHORT_EXPECTED_FRAMES = 30;
+
     public static final String PACKAGE_NAME = "com.android.test.uibench";
 
-    private static UiBenchJankTestsHelper mInstance;
-    private static UiDevice mDevice;
+    private static final int SLOW_FLING_SPEED = 3000; // compare to UiObject2#DEFAULT_FLING_SPEED
+
+    private static UiBenchJankTestsHelper sInstance;
+    private UiDevice mDevice;
     private Context mContext;
+    private DisplayMetrics mDisplayMetrics;
     protected UiObject2 mContents;
 
     private UiBenchJankTestsHelper(Context context, UiDevice device) {
         mContext = context;
         mDevice = device;
+        mDisplayMetrics = context.getResources().getDisplayMetrics();
     }
 
     public static UiBenchJankTestsHelper getInstance(Context context, UiDevice device) {
-        if (mInstance == null) {
-            mInstance = new UiBenchJankTestsHelper(context, device);
+        if (sInstance == null) {
+            sInstance = new UiBenchJankTestsHelper(context, device);
         }
-        return mInstance;
+        return sInstance;
     }
 
     /**
      * Launch activity using intent
-     * @param activityName
-     * @param verifyText
      */
     public void launchActivity(String activityName, String verifyText) {
         ComponentName cn = new ComponentName(PACKAGE_NAME,
@@ -84,21 +95,35 @@
     /**
      * To perform the fling down and up on given content for flingCount number
      * of times
-     * @param content
-     * @param timeout
-     * @param flingCount
      */
-    public void flingUpDown(UiObject2 content, long timeout, int flingCount) {
-        flingUpDown(content, timeout, flingCount, false);
+    public void flingUpDown(UiObject2 content, int flingCount) {
+        flingUpDown(content, flingCount, false);
     }
 
-    public void flingUpDown(UiObject2 content, long timeout, int flingCount, boolean reverse) {
+    public void flingUpDown(UiObject2 content, int flingCount, boolean reverse) {
         for (int count = 0; count < flingCount; count++) {
-            SystemClock.sleep(timeout);
+            SystemClock.sleep(SHORT_TIMEOUT);
             content.fling(reverse ? Direction.UP : Direction.DOWN);
-            SystemClock.sleep(timeout);
+            SystemClock.sleep(SHORT_TIMEOUT);
             content.fling(reverse ? Direction.DOWN : Direction.UP);
         }
     }
 
+    /**
+     * To perform the swipe right and left on given content for swipeCount number
+     * of times
+     */
+    public void swipeRightLeft(UiObject2 content, int swipeCount) {
+        for (int count = 0; count < swipeCount; count++) {
+            SystemClock.sleep(SHORT_TIMEOUT);
+            content.swipe(Direction.RIGHT, 1);
+            SystemClock.sleep(SHORT_TIMEOUT);
+            content.swipe(Direction.LEFT, 1);
+        }
+    }
+
+    public void slowSingleFlingDown(UiObject2 content) {
+        SystemClock.sleep(SHORT_TIMEOUT);
+        content.fling(Direction.DOWN, (int)(SLOW_FLING_SPEED * mDisplayMetrics.density));
+    }
 }
diff --git a/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchRenderingJankTests.java b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchRenderingJankTests.java
index c825116..7a088f0 100644
--- a/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchRenderingJankTests.java
+++ b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchRenderingJankTests.java
@@ -63,7 +63,7 @@
     @JankTest(beforeTest = "openBitmapUpload", expectedFrames = EXPECTED_FRAMES)
     @GfxMonitor(processName = PACKAGE_NAME)
     public void testBitmapUploadJank() {
-        SystemClock.sleep(mHelper.LONG_TIMEOUT * 5);
+        SystemClock.sleep(UiBenchJankTestsHelper.FULL_TEST_DURATION);
     }
 
     // Open Shadow Grid
@@ -79,7 +79,7 @@
     @JankTest(beforeTest = "openRenderingList", expectedFrames = EXPECTED_FRAMES)
     @GfxMonitor(processName = PACKAGE_NAME)
     public void testShadowGridListFling() {
-        mHelper.flingUpDown(mHelper.mContents, mHelper.SHORT_TIMEOUT, 1);
+        mHelper.flingUpDown(mHelper.mContents, 1);
     }
 
 }
diff --git a/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchTextJankTests.java b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchTextJankTests.java
index b730aec..1e8f8d7 100644
--- a/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchTextJankTests.java
+++ b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchTextJankTests.java
@@ -60,11 +60,12 @@
     }
 
     // Measure jank metrics for EditText Typing
-    @JankTest(beforeTest = "openEditTextTyping", expectedFrames = EXPECTED_FRAMES)
-    @GfxMonitor(processName = PACKAGE_NAME)
-    public void testEditTextTyping() {
-        SystemClock.sleep(mHelper.LONG_TIMEOUT * 2);
-    }
+    // Reenable the test after b/62917134 is fixed
+    // @JankTest(beforeTest = "openEditTextTyping", expectedFrames = EXPECTED_FRAMES)
+    // @GfxMonitor(processName = PACKAGE_NAME)
+    // public void testEditTextTyping() {
+    //    SystemClock.sleep(UiBenchJankTestsHelper.FULL_TEST_DURATION);
+    //}
 
     // Open Layout Cache High Hitrate
     public void openLayoutCacheHighHitrate() {
@@ -80,7 +81,7 @@
     @JankTest(beforeTest = "openLayoutCacheHighHitrate", expectedFrames = EXPECTED_FRAMES)
     @GfxMonitor(processName = PACKAGE_NAME)
     public void testLayoutCacheHighHitrateFling() {
-        mHelper.flingUpDown(mHelper.mContents, mHelper.SHORT_TIMEOUT, 3);
+        mHelper.flingUpDown(mHelper.mContents, 3);
     }
 
     // Open Layout Cache Low Hitrate
@@ -97,7 +98,7 @@
     @JankTest(beforeTest = "openLayoutCacheLowHitrate", expectedFrames = EXPECTED_FRAMES)
     @GfxMonitor(processName = PACKAGE_NAME)
     public void testLayoutCacheLowHitrateFling() {
-        mHelper.flingUpDown(mHelper.mContents, mHelper.SHORT_TIMEOUT, 3);
+        mHelper.flingUpDown(mHelper.mContents, 3);
     }
 
 }
diff --git a/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchWebView.java b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchWebView.java
index 659577c..28c2ed3 100644
--- a/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchWebView.java
+++ b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchWebView.java
@@ -62,7 +62,7 @@
     @JankTest(beforeTest = "openScrollableWebView", expectedFrames = EXPECTED_FRAMES)
     @GfxMonitor(processName = PACKAGE_NAME)
     public void testWebViewFling() {
-        mHelper.flingUpDown(mHelper.mContents, mHelper.SHORT_TIMEOUT, 1);
+        mHelper.flingUpDown(mHelper.mContents, 1);
     }
 
 }
diff --git a/tests/jank/uibench_wear/Android.mk b/tests/jank/uibench_wear/Android.mk
index acc38d6..6310569 100644
--- a/tests/jank/uibench_wear/Android.mk
+++ b/tests/jank/uibench_wear/Android.mk
@@ -28,4 +28,6 @@
 
 LOCAL_SDK_VERSION := current
 
+LOCAL_COMPATIBILITY_SUITE := device-tests
+
 include $(BUILD_PACKAGE)
diff --git a/tests/jank/uibench_wear/AndroidTest.xml b/tests/jank/uibench_wear/AndroidTest.xml
new file mode 100644
index 0000000..3d1af6a
--- /dev/null
+++ b/tests/jank/uibench_wear/AndroidTest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Runs Wearable Platform UiBench Jank Tests.">
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+        <option name="test-file-name" value="UiBenchJankTestsWear.apk" />
+        <option name="test-file-name" value="UiBench.apk" />
+    </target_preparer>
+
+    <option name="post-boot-command" value="am broadcast -a com.google.android.clockwork.action.TEST_MODE" />
+    <option name="post-boot-command" value="setprop debug.hwui.filter_test_overhead true" />
+    <option name="post-boot-command" value="settings put secure accessibility_disable_animations 0" />
+
+    <option name="test-tag" value="UiBenchJankTestsWear" />
+    <test class="com.android.tradefed.testtype.InstrumentationTest">
+        <option name="package" value="com.android.wearable.uibench.janktests" />
+        <option name="runner" value="android.test.InstrumentationTestRunner" />
+    </test>
+</configuration>
diff --git a/tests/jank/webview/Android.mk b/tests/jank/webview/Android.mk
index a3ba802..ccae53b 100644
--- a/tests/jank/webview/Android.mk
+++ b/tests/jank/webview/Android.mk
@@ -26,4 +26,6 @@
 
 LOCAK_SDK_VERSION := current
 
+LOCAL_COMPATIBILITY_SUITE := device-tests
+
 include $(BUILD_PACKAGE)
diff --git a/tests/jank/webview/AndroidTest.xml b/tests/jank/webview/AndroidTest.xml
new file mode 100644
index 0000000..81fd1cd
--- /dev/null
+++ b/tests/jank/webview/AndroidTest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Runs Chromium Jank Tests.">
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+        <option name="test-file-name" value="UbWebViewJankTests.apk" />
+    </target_preparer>
+
+    <option name="post-boot-command" value="am broadcast -a com.google.android.clockwork.action.TEST_MODE" />
+    <option name="post-boot-command" value="setprop debug.hwui.filter_test_overhead true" />
+    <option name="post-boot-command" value="settings put secure accessibility_disable_animations 0" />
+
+    <option name="test-tag" value="UbWebViewJankTests" />
+    <test class="com.android.tradefed.testtype.InstrumentationTest">
+        <option name="package" value="com.android.webview.chromium.tests.jank" />
+        <option name="runner" value="android.test.InstrumentationTestRunner" />
+    </test>
+</configuration>
diff --git a/libraries/base-app-helpers/Android.mk b/tests/perf/BootHelperApp/Android.mk
similarity index 60%
copy from libraries/base-app-helpers/Android.mk
copy to tests/perf/BootHelperApp/Android.mk
index 0111a0a..221df3d 100644
--- a/libraries/base-app-helpers/Android.mk
+++ b/tests/perf/BootHelperApp/Android.mk
@@ -1,24 +1,30 @@
-#
-# Copyright (C) 2016 The Android Open Source Project
+# Copyright 2016 Google Inc. All Rights Reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
 # You may obtain a copy of the License at
 #
-#      http://www.apache.org/licenses/LICENSE-2.0
+#     http://www.apache.org/licenses/LICENSE-2.0
 #
 # Unless required by applicable law or agreed to in writing, software
 # distributed under the License is distributed on an "AS IS" BASIS,
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-#
-LOCAL_PATH := $(call my-dir)
 
+LOCAL_PATH := $(call my-dir)
 include $(CLEAR_VARS)
-LOCAL_MODULE := base-app-helpers
-LOCAL_STATIC_JAVA_LIBRARIES := dpad-util
-LOCAL_JAVA_LIBRARIES := ub-uiautomator launcher-helper-lib
+
+LOCAL_MODULE_TAGS := tests
+
+# Only compile source java files in this apk.
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
-include $(BUILD_STATIC_JAVA_LIBRARY)
+LOCAL_PACKAGE_NAME := BootHelperApp
+LOCAL_JAVA_LIBRARIES := android.test.runner
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-test ub-uiautomator
+
+LOCAL_COMPATIBILITY_SUITE := device-tests
+
+include $(BUILD_PACKAGE)
+
diff --git a/tests/perf/BootHelperApp/AndroidManifest.xml b/tests/perf/BootHelperApp/AndroidManifest.xml
new file mode 100644
index 0000000..4539d32
--- /dev/null
+++ b/tests/perf/BootHelperApp/AndroidManifest.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT 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.android.boothelper"
+    android:sharedUserId="com.android.boothelper"
+    android:versionCode="1"
+    android:versionName="1.0" >
+
+    <instrumentation
+        android:name="android.support.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.boothelper" />
+
+    <application>
+        <activity android:name=".AwareActivity"
+                android:directBootAware="true"
+                android:exported="true">
+        </activity>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+</manifest>
diff --git a/tests/perf/BootHelperApp/AndroidTest.xml b/tests/perf/BootHelperApp/AndroidTest.xml
new file mode 100644
index 0000000..71f1288
--- /dev/null
+++ b/tests/perf/BootHelperApp/AndroidTest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Runs BootHelperApp Tests.">
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+        <option name="test-file-name" value="BootHelperApp.apk" />
+    </target_preparer>
+
+    <option name="test-tag" value="BootHelperApp" />
+    <test class="com.android.tradefed.testtype.InstrumentationTest" >
+        <option name="package" value="com.android.boothelper" />
+        <option name="runner" value="android.support.test.runner.AndroidJUnitRunner" />
+    </test>
+</configuration>
diff --git a/tests/jank/notificationsgenerator_wear/src/com/google/android/wearable/support/CustomNotificationRemoteInputActivity.java b/tests/perf/BootHelperApp/src/com/android/boothelper/AwareActivity.java
similarity index 63%
copy from tests/jank/notificationsgenerator_wear/src/com/google/android/wearable/support/CustomNotificationRemoteInputActivity.java
copy to tests/perf/BootHelperApp/src/com/android/boothelper/AwareActivity.java
index b58f8df..467f203 100644
--- a/tests/jank/notificationsgenerator_wear/src/com/google/android/wearable/support/CustomNotificationRemoteInputActivity.java
+++ b/tests/perf/BootHelperApp/src/com/android/boothelper/AwareActivity.java
@@ -1,5 +1,4 @@
 /*
-
  * Copyright (C) 2016 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -15,10 +14,18 @@
  * limitations under the License.
  */
 
-package com.google.android.wearable.support;
+package com.android.boothelper;
 
 import android.app.Activity;
+import android.os.Bundle;
 
-public class CustomNotificationRemoteInputActivity extends Activity{
-
+/**
+ * Activity that enables the boot aware property which is needed for
+ * unlocking the device through automation when the device is FBE encrypted
+ */
+public class AwareActivity extends Activity {
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+    }
 }
diff --git a/tests/perf/BootHelperApp/src/com/android/boothelper/BootHelperTest.java b/tests/perf/BootHelperApp/src/com/android/boothelper/BootHelperTest.java
new file mode 100644
index 0000000..cec70ba
--- /dev/null
+++ b/tests/perf/BootHelperApp/src/com/android/boothelper/BootHelperTest.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.boothelper;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.app.admin.DevicePolicyManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.util.Log;
+import android.view.KeyEvent;
+
+import java.util.concurrent.CountDownLatch;
+
+import org.junit.Before;
+import org.junit.Test;
+
+
+
+public class BootHelperTest {
+
+    private static final long TIMEOUT = 10000;
+    private static final String TAG = "BootHelperTest";
+    private static final String SETTINGS_PKG = "com.android.settings";
+    private static final String LOCK_PIN_ID = "lock_pin";
+    private static final String REQUIRE_PWD_ID = "encrypt_dont_require_password";
+    private static final String PWD_ENTRY = "password_entry";
+    private UiDevice mDevice;
+    private Context mProtectedContext;
+
+    @Before
+    public void setUp() throws Exception {
+        mDevice = UiDevice.getInstance(getInstrumentation());
+        mProtectedContext = getInstrumentation().getContext()
+                .createDeviceProtectedStorageContext();
+    }
+
+    @Test
+    public void setupLockScreenPin() throws Exception {
+        Activity activity = launchActivity(getInstrumentation().getTargetContext()
+                .getPackageName(), AwareActivity.class, new Intent(Intent.ACTION_MAIN));
+        mDevice.waitForIdle();
+
+        // Set a PIN for this user
+        final Intent intent = new Intent(DevicePolicyManager.ACTION_SET_NEW_PASSWORD);
+        intent.addCategory(Intent.CATEGORY_DEFAULT);
+        activity.startActivity(intent);
+        mDevice.waitForIdle();
+
+        // Pick PIN from the option list
+        selectOption(LOCK_PIN_ID);
+
+        // Ignore any interstitial options
+        selectOption(REQUIRE_PWD_ID);
+
+        // Set our PIN
+        selectOption(PWD_ENTRY);
+
+        // Enter it twice to confirm
+        enterTestPin();
+        enterTestPin();
+        mDevice.pressBack();
+
+    }
+
+    @Test
+    public void unlockScreenWithPin() throws Exception {
+        final CountDownLatch latch = new CountDownLatch(1);
+        final BroadcastReceiver receiver = new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                latch.countDown();
+            }
+        };
+        mProtectedContext.registerReceiver(receiver, new IntentFilter(
+                Intent.ACTION_USER_UNLOCKED));
+        dismissKeyguard();
+    }
+
+    private void dismissKeyguard() throws Exception {
+        mDevice.wakeUp();
+        mDevice.waitForIdle();
+        mDevice.pressMenu();
+        mDevice.waitForIdle();
+        enterTestPin();
+    }
+
+    private void enterTestPin() throws Exception {
+        mDevice.waitForIdle();
+        mDevice.pressKeyCode(KeyEvent.KEYCODE_1);
+        mDevice.pressKeyCode(KeyEvent.KEYCODE_2);
+        mDevice.pressKeyCode(KeyEvent.KEYCODE_3);
+        mDevice.pressKeyCode(KeyEvent.KEYCODE_4);
+        mDevice.pressKeyCode(KeyEvent.KEYCODE_5);
+        mDevice.waitForIdle();
+        mDevice.pressEnter();
+        Log.i(TAG, "Screen Unlocked");
+        mDevice.waitForIdle();
+    }
+
+    /**
+     * Return the instrumentation from the registry.
+     *
+     * @return
+     */
+    private Instrumentation getInstrumentation() {
+        return InstrumentationRegistry.getInstrumentation();
+    }
+
+    /**
+     * Click on the option based on the resource id in the settings package.
+     *
+     * @param optionId
+     */
+    public void selectOption(String optionId) {
+        UiObject2 tos = mDevice.wait(Until.findObject(By.res(SETTINGS_PKG, optionId)),
+                TIMEOUT);
+        if (tos != null) {
+            tos.click();
+        }
+    }
+
+    /**
+     * To launch an activity
+     * @param pkg
+     * @param activityCls
+     * @param intent
+     * @return
+     */
+    public Activity launchActivity(String pkg, Class activityCls, Intent intent) {
+        intent.setClassName(pkg, activityCls.getName());
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        return getInstrumentation().startActivitySync(intent);
+    }
+
+
+}
diff --git a/tests/perf/PerfTransitionTest/Android.mk b/tests/perf/PerfTransitionTest/Android.mk
index b41d0e7..8cac0dd 100644
--- a/tests/perf/PerfTransitionTest/Android.mk
+++ b/tests/perf/PerfTransitionTest/Android.mk
@@ -25,5 +25,9 @@
 LOCAL_STATIC_JAVA_LIBRARIES := \
     android-support-test \
     ub-uiautomator \
-    launcher-helper-lib
+    launcher-helper-lib \
+    sysui-helper
+
+LOCAL_COMPATIBILITY_SUITE := device-tests
+
 include $(BUILD_PACKAGE)
diff --git a/tests/perf/PerfTransitionTest/AndroidTest.xml b/tests/perf/PerfTransitionTest/AndroidTest.xml
new file mode 100644
index 0000000..b4f361b
--- /dev/null
+++ b/tests/perf/PerfTransitionTest/AndroidTest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Runs AppTransitionTests.">
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+        <option name="test-file-name" value="AppTransitionTests.apk" />
+    </target_preparer>
+
+    <option name="test-tag" value="AppTransitionTests" />
+    <test class="com.android.tradefed.testtype.InstrumentationTest" >
+        <option name="package" value="com.android.apptransition.tests" />
+        <option name="runner" value="android.support.test.runner.AndroidJUnitRunner" />
+    </test>
+</configuration>
diff --git a/tests/perf/PerfTransitionTest/src/com/android/apptransition/tests/AppTransitionTests.java b/tests/perf/PerfTransitionTest/src/com/android/apptransition/tests/AppTransitionTests.java
index eafb8d8..38eff09 100644
--- a/tests/perf/PerfTransitionTest/src/com/android/apptransition/tests/AppTransitionTests.java
+++ b/tests/perf/PerfTransitionTest/src/com/android/apptransition/tests/AppTransitionTests.java
@@ -16,18 +16,14 @@
 
 package com.android.apptransition.tests;
 
-import android.app.ActivityManagerNative;
+import android.app.ActivityManager;
 import android.app.IActivityManager;
 import android.app.Instrumentation;
-import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
-import android.content.pm.ActivityInfo;
 import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.ResolveInfo;
 import android.os.Bundle;
-import android.os.Environment;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
 import android.support.test.InstrumentationRegistry;
@@ -36,7 +32,6 @@
 import android.support.test.rule.logging.AtraceLogger;
 import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.UiDevice;
-import android.support.test.uiautomator.UiObjectNotFoundException;
 import android.support.test.uiautomator.UiObject2;
 import android.support.test.uiautomator.Until;
 import android.util.Log;
@@ -47,8 +42,8 @@
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.IOException;
-import java.util.ArrayList;
 import java.util.HashSet;
+import java.util.HashMap;
 import java.util.List;
 import java.util.LinkedHashMap;
 import java.util.Map;
@@ -62,6 +57,7 @@
 
     private static final String TAG = AppTransitionTests.class.getSimpleName();
     private static final int JOIN_TIMEOUT = 10000;
+    private static final int DEFAULT_DROP_CACHE_DELAY = 2000;
     private static final String DEFAULT_POST_LAUNCH_TIMEOUT = "5000";
     private static final String DEFAULT_LAUNCH_COUNT = "10";
     private static final String SUCCESS_MESSAGE = "Status: ok";
@@ -110,13 +106,14 @@
     private Set<String> mTraceCategoriesSet = null;
     private AtraceLogger mAtraceLogger = null;
     private String mComponentName = null;
+    private Map<String,String> mPreAppsComponentName = new HashMap<String, String>();
 
     @Before
     public void setUp() throws Exception {
         mPackageManager = getInstrumentation().getContext().getPackageManager();
         mContext = getInstrumentation().getContext();
         mArgs = InstrumentationRegistry.getArguments();
-        mActivityManager = ActivityManagerNative.getDefault();
+        mActivityManager = ActivityManager.getService();
         mDevice = UiDevice.getInstance(getInstrumentation());
         mLauncherStrategy = LauncherStrategyFactory.getInstance(mDevice).getLauncherStrategy();
         createLaunchIntentMappings();
@@ -151,6 +148,7 @@
             }
         }
         mDevice.setOrientationNatural();
+        sleep(mPostLaunchTimeout);
         cleanTestApps();
     }
 
@@ -175,26 +173,38 @@
         // Perform cold app launch from launcher screen
         for (int appCount = 0; appCount < mAppListArray.length; appCount++) {
             String appName = mAppListArray[appCount];
-            for (int launchCount = 0; launchCount < mLaunchIterations; launchCount++) {
+            // Additional launch to account for cold launch
+            if (setupAppLaunch(appName) == ILauncherStrategy.LAUNCH_FAILED_TIMESTAMP) {
+                continue;
+            }
+            closeApps(new String[] {
+                    appName
+            });
+            getInstrumentation().getUiAutomation()
+                    .executeShellCommand(DROP_CACHE_SCRIPT);
+            sleep(DEFAULT_DROP_CACHE_DELAY);
+            for (int launchCount = 0; launchCount <= mLaunchIterations; launchCount++) {
                 if (null != mAtraceLogger) {
                     mAtraceLogger.atraceStart(mTraceCategoriesSet, mTraceBufferSize,
                             mTraceDumpInterval, mRootTraceSubDir,
                             String.format("%s-%d", appName, launchCount));
                 }
-                mLauncherStrategy.launch(appName, mAppLaunchIntentsMapping.get(appName).
-                        getComponent().flattenToShortString().split("\\/")[0]);
+                mLauncherStrategy.launch(appName, mComponentName.split("\\/")[0]);
                 if (null != mAtraceLogger) {
                     mAtraceLogger.atraceStop();
                 }
                 sleep(mPostLaunchTimeout);
+                mDevice.pressHome();
+		mDevice.waitForIdle();
                 closeApps(new String[] {
                         appName
                 });
-                pressUiHome();
                 sleep(mPostLaunchTimeout);
                 getInstrumentation().getUiAutomation()
                         .executeShellCommand(DROP_CACHE_SCRIPT);
+                sleep(DEFAULT_DROP_CACHE_DELAY);
             }
+            mComponentName = null;
             // Update the result with the component name
             updateResult(appName);
         }
@@ -215,12 +225,12 @@
         }
         for (int appCount = 0; appCount < mAppListArray.length; appCount++) {
             String appName = mAppListArray[appCount];
-            // Additional launches to account for cold launch and one hot launch.
+            // Additional launch to account for cold launch
             if (setupAppLaunch(appName) == ILauncherStrategy.LAUNCH_FAILED_TIMESTAMP) {
                 continue;
             }
-            // Hot app launch for given launch iterations.
-            for (int launchCount = 0; launchCount < (mLaunchIterations); launchCount++) {
+            // Hot app launch for given (launch iterations + 1) times.
+            for (int launchCount = 0; launchCount <= (mLaunchIterations); launchCount++) {
                 if (null != mAtraceLogger) {
                     mAtraceLogger.atraceStart(mTraceCategoriesSet, mTraceBufferSize,
                             mTraceDumpInterval, mRootTraceSubDir,
@@ -231,9 +241,10 @@
                     mAtraceLogger.atraceStop();
                 }
                 sleep(mPostLaunchTimeout);
-                pressUiHome();
+                mDevice.pressHome();
                 sleep(mPostLaunchTimeout);
             }
+            mComponentName = null;
             // Update the result with the component name
             updateResult(appName);
         }
@@ -256,24 +267,14 @@
         }
         mPreAppsList = mPreAppsList.replaceAll("%"," ");
         mPreAppsListArray = mPreAppsList.split(DELIMITER);
+        mPreAppsComponentName.clear();
         populateRecentsList();
-        // Increment by one to account for the first cold launch and discard it
-        // while parsing
-        mLaunchIterations++;
         for (int appCount = 0; appCount < mAppListArray.length; appCount++) {
             String appName = mAppListArray[appCount];
             long appLaunchTime = -1L;
-            for (int launchCount = 0; launchCount < mLaunchIterations; launchCount++) {
-                // "NOT_SURE" for first launch because apps in the PreAppsList
-                // could be part of AppList
-                if (launchCount == 0) {
-                    appLaunchTime = startApp(appName, NOT_SURE);
-                } else {
-                    appLaunchTime = startApp(appName, HOT_LAUNCH);
-                }
-                if (appLaunchTime == ILauncherStrategy.LAUNCH_FAILED_TIMESTAMP) {
-                    break;
-                }
+            for (int launchCount = 0; launchCount <= mLaunchIterations; launchCount++) {
+                mLauncherStrategy.launch(appName, mPreAppsComponentName.get(appName).split(
+                        "\\/")[0]);
                 sleep(mPostLaunchTimeout);
                 if (null != mAtraceLogger && launchCount > 0) {
                     mAtraceLogger.atraceStart(mTraceCategoriesSet, mTraceBufferSize,
@@ -285,7 +286,7 @@
                 if (null != mAtraceLogger && launchCount > 0) {
                     mAtraceLogger.atraceStop();
                 }
-                pressUiHome();
+                mDevice.pressHome();
                 sleep(mPostLaunchTimeout);
             }
             updateResult(appName);
@@ -310,14 +311,15 @@
         }
         mPreAppsList = mPreAppsList.replaceAll("%", " ");
         mPreAppsListArray = mPreAppsList.split(DELIMITER);
+        mPreAppsComponentName.clear();
         populateRecentsList();
         for (int appCount = 0; appCount < mAppListArray.length; appCount++) {
             String appName = mAppListArray[appCount];
-            // Additional launches to account for cold launch and one hot launch.
-            if (setupAppLaunch(appName) == ILauncherStrategy.LAUNCH_FAILED_TIMESTAMP) {
-                continue;
-            }
-            for (int launchCount = 0; launchCount < (mLaunchIterations); launchCount++) {
+            // To bring the app to launch as first item from recents task.
+            mLauncherStrategy.launch(appName, mPreAppsComponentName.get(appName).split(
+                    "\\/")[0]);
+            sleep(mPostLaunchTimeout);
+            for (int launchCount = 0; launchCount <= mLaunchIterations; launchCount++) {
                 if (null != mAtraceLogger) {
                     mAtraceLogger.atraceStart(mTraceCategoriesSet, mTraceBufferSize,
                             mTraceDumpInterval, mRootTraceSubDir,
@@ -328,7 +330,7 @@
                 if (null != mAtraceLogger) {
                     mAtraceLogger.atraceStop();
                 }
-                pressUiHome();
+                mDevice.pressHome();
                 sleep(mPostLaunchTimeout);
             }
             updateResult(appName);
@@ -336,8 +338,8 @@
     }
 
     /**
-     * Launch given app couple of times to account for the cold launch and one hot launch and
-     * to update component name associated with the hot launch of the given app.
+     * Launch given app to account for the cold launch and track
+     * component name associated with the app.
      * @throws RemoteException if press home is not successful
      * @param appName
      * @return
@@ -348,13 +350,8 @@
             return appLaunchTime;
         }
         sleep(mPostLaunchTimeout);
-        pressUiHome();
-        appLaunchTime = startApp(appName, HOT_LAUNCH);
-        if (appLaunchTime == ILauncherStrategy.LAUNCH_FAILED_TIMESTAMP) {
-            return appLaunchTime;
-        }
+        mDevice.pressHome();
         sleep(mPostLaunchTimeout);
-        pressUiHome();
         return appLaunchTime;
     }
 
@@ -416,7 +413,8 @@
         closeApps(mAppListArray);
         getInstrumentation().getUiAutomation()
                         .executeShellCommand(DROP_CACHE_SCRIPT);
-        pressUiHome();
+        sleep(DEFAULT_DROP_CACHE_DELAY);
+        mDevice.pressHome();
         sleep(mPostLaunchTimeout);
     }
 
@@ -426,11 +424,13 @@
      */
     private void populateRecentsList() throws RemoteException {
         for (int preAppCount = 0; preAppCount < mPreAppsListArray.length; preAppCount++) {
-            startApp(mPreAppsListArray[preAppCount], COLD_LAUNCH);
+            startApp(mPreAppsListArray[preAppCount], NOT_SURE);
+            mPreAppsComponentName.put(mPreAppsListArray[preAppCount], mComponentName);
             sleep(mPostLaunchTimeout);
-            pressUiHome();
+            mDevice.pressHome();
             sleep(mPostLaunchTimeout);
         }
+        mComponentName = null;
     }
 
 
@@ -512,7 +512,7 @@
         @Override
         public void run() {
             String packageName = mLaunchIntent.getComponent().getPackageName();
-            String componentName = mLaunchIntent.getComponent().flattenToShortString();
+            String componentName = mLaunchIntent.getComponent().flattenToString();
             String launchCmd = String.format("%s %s", APP_LAUNCH_CMD, componentName);
             ParcelFileDescriptor parcelDesc = getInstrumentation().getUiAutomation()
                     .executeShellCommand(launchCmd);
@@ -555,7 +555,7 @@
                     // Needed to update the component name if the very first launch activity
                     // is different from hot launch activity (i.e YouTube)
                     if ((launchSuccess && (mLaunchMode.contains(HOT_LAUNCH) ||
-                            mLaunchMode.contains(NOT_SURE)) && lineCount == 4)) {
+                            mLaunchMode.contains(NOT_SURE)) && lineCount == 3)) {
                         String activitySplit[] = line.split(":");
                         if (activitySplit[0].contains(ACTIVITY)) {
                             mCmpName = activitySplit[1].trim();
@@ -607,7 +607,7 @@
             } else {
                 // Component name needed for parsing the events log
                 mResult.putString(appName, mAppLaunchIntentsMapping.get(appName).
-                        getComponent().flattenToShortString());
+                        getComponent().flattenToString());
             }
     }
 
diff --git a/tests/perf/PerfTransitionTest/src/com/android/apptransition/tests/LatencyTests.java b/tests/perf/PerfTransitionTest/src/com/android/apptransition/tests/LatencyTests.java
new file mode 100644
index 0000000..f8b1f48
--- /dev/null
+++ b/tests/perf/PerfTransitionTest/src/com/android/apptransition/tests/LatencyTests.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.apptransition.tests;
+
+import static android.support.test.InstrumentationRegistry.getInstrumentation;
+
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.provider.Settings;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.system.helpers.LockscreenHelper;
+import android.system.helpers.OverviewHelper;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Tests to test various latencies in the system.
+ */
+public class LatencyTests {
+
+    private static final int DEFAULT_ITERATION_COUNT = 10;
+    private static final String KEY_ITERATION_COUNT = "iteration_count";
+    private static final long CLOCK_SETTLE_DELAY = 2000;
+    private static final String FINGERPRINT_WAKE_FAKE_COMMAND = "am broadcast -a "
+            + "com.android.systemui.latency.ACTION_FINGERPRINT_WAKE";
+    private static final String TURN_ON_SCREEN_COMMAND = "am broadcast -a "
+            + "com.android.systemui.latency.ACTION_TURN_ON_SCREEN";
+    private static final String AM_START_COMMAND_TEMPLATE = "am start -a %s";
+    private static final String PIN = "1234";
+
+    private UiDevice mDevice;
+    private int mIterationCount;
+
+    @Before
+    public void setUp() throws Exception {
+        mDevice = UiDevice.getInstance(getInstrumentation());
+        Bundle args = InstrumentationRegistry.getArguments();
+        mIterationCount = Integer.parseInt(args.getString(KEY_ITERATION_COUNT,
+                Integer.toString(DEFAULT_ITERATION_COUNT)));
+        mDevice.pressHome();
+    }
+
+    /**
+     * Test to track how long it takes to expand the notification shade when swiping.
+     * <p>
+     * Every iteration will output a log in the form of "LatencyTracker/action=0 delay=x".
+     */
+    @Test
+    public void testExpandNotificationsLatency() throws Exception {
+        for (int i = 0; i < mIterationCount; i++) {
+            swipeDown();
+            mDevice.waitForIdle();
+            swipeUp();
+            mDevice.waitForIdle();
+
+            // Wait for clocks to settle down
+            SystemClock.sleep(CLOCK_SETTLE_DELAY);
+        }
+    }
+
+    private void swipeDown() {
+        mDevice.swipe(mDevice.getDisplayWidth() / 2,
+                0, mDevice.getDisplayWidth() / 2,
+                mDevice.getDisplayHeight() / 2,
+                15);
+    }
+
+    private void swipeUp() {
+        mDevice.swipe(mDevice.getDisplayWidth() / 2,
+                mDevice.getDisplayHeight() / 2,
+                mDevice.getDisplayWidth() / 2,
+                0,
+                15);
+    }
+
+    /**
+     * Test to track how long it takes until the animation starts in a fingerprint-wake-and-unlock
+     * sequence.
+     * <p>
+     * Every iteration will output a log in the form of "LatencyTracker/action=2 delay=x".
+     */
+    @Test
+    public void testFingerprintWakeAndUnlock() throws Exception {
+        for (int i = 0; i < mIterationCount; i++) {
+            mDevice.sleep();
+
+            // Wait for clocks to settle down
+            SystemClock.sleep(CLOCK_SETTLE_DELAY);
+
+            mDevice.executeShellCommand(FINGERPRINT_WAKE_FAKE_COMMAND);
+            mDevice.waitForIdle();
+        }
+    }
+
+    /**
+     * Test how long it takes until the screen is fully turned on.
+     * <p>
+     * Every iteration will output a log in the form of "LatencyTracker/action=5 delay=x".
+     */
+    @Test
+    public void testScreenTurnOn() throws Exception {
+        for (int i = 0; i < mIterationCount; i++) {
+            mDevice.sleep();
+
+            // Wait for clocks to settle down
+            SystemClock.sleep(CLOCK_SETTLE_DELAY);
+
+            mDevice.executeShellCommand(TURN_ON_SCREEN_COMMAND);
+            mDevice.waitForIdle();
+        }
+
+        // Put device to home screen.
+        mDevice.pressMenu();
+        mDevice.waitForIdle();
+    }
+
+    /**
+     * Test how long it takes until the credential (PIN) is checked.
+     * <p>
+     * Every iteration will output a log in the form of "LatencyTracker/action=3 delay=x".
+     */
+    @Test
+    public void testPinCheckDelay() throws Exception {
+        LockscreenHelper.getInstance().setScreenLockViaShell(PIN, LockscreenHelper.MODE_PIN);
+        for (int i = 0; i < mIterationCount; i++) {
+            mDevice.sleep();
+
+            // Make sure not to launch camera with "double-tap".
+            Thread.sleep(300);
+            mDevice.wakeUp();
+            LockscreenHelper.getInstance().unlockScreen(PIN);
+            mDevice.waitForIdle();
+        }
+        LockscreenHelper.getInstance().removeScreenLockViaShell(PIN);
+        mDevice.pressHome();
+    }
+
+    /**
+     * Test that measure how long the total time takes until recents is visible after pressing it.
+     * Note that this is different from {@link AppTransitionTests#testAppToRecents} as we are
+     * measuring the full latency here, but in the app transition test we only measure the time
+     * spent after startActivity is called. This might be different as SystemUI does a lot of binder
+     * calls before calling startActivity.
+     * <p>
+     * Every iteration will output a log in the form of "LatencyTracker/action=1 delay=x".
+     */
+    @Test
+    public void testAppToRecents() throws Exception {
+        OverviewHelper.getInstance().populateManyRecentApps();
+        for (int i = 0; i < mIterationCount; i++) {
+            mDevice.executeShellCommand(String.format(AM_START_COMMAND_TEMPLATE,
+                    Settings.ACTION_SETTINGS));
+            mDevice.waitForIdle();
+
+            // Wait for clocks to settle.
+            SystemClock.sleep(CLOCK_SETTLE_DELAY);
+            pressUiRecentApps();
+            mDevice.waitForIdle();
+
+            // Make sure all the animations are really done.
+            SystemClock.sleep(200);
+        }
+    }
+
+    private void pressUiRecentApps() throws Exception {
+        mDevice.findObject(By.res("com.android.systemui", "recent_apps")).click();
+    }
+}
diff --git a/tests/perf/PerformanceAppTest/Android.mk b/tests/perf/PerformanceAppTest/Android.mk
index 33ea105..54882f9 100644
--- a/tests/perf/PerformanceAppTest/Android.mk
+++ b/tests/perf/PerformanceAppTest/Android.mk
@@ -25,6 +25,8 @@
 LOCAL_JAVA_LIBRARIES := legacy-android-test
 LOCAL_STATIC_JAVA_LIBRARIES := android-support-test
 
+LOCAL_COMPATIBILITY_SUITE := device-tests
+
 include $(BUILD_PACKAGE)
 
 # Use the following include to make our test apk.
diff --git a/tests/perf/PerformanceAppTest/AndroidTest.xml b/tests/perf/PerformanceAppTest/AndroidTest.xml
new file mode 100644
index 0000000..2d56796
--- /dev/null
+++ b/tests/perf/PerformanceAppTest/AndroidTest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Runs PerformanceAppTest.">
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+        <option name="test-file-name" value="PerformanceAppTest.apk" />
+    </target_preparer>
+
+    <option name="test-tag" value="PerformanceAppTest" />
+    <test class="com.android.tradefed.testtype.InstrumentationTest" >
+        <option name="package" value="com.android.performanceapp.tests" />
+        <option name="runner" value="android.support.test.runner.AndroidJUnitRunner" />
+    </test>
+</configuration>
diff --git a/tests/perf/PerformanceLaunch/Android.mk b/tests/perf/PerformanceLaunch/Android.mk
index 15d8289..f3604ed 100644
--- a/tests/perf/PerformanceLaunch/Android.mk
+++ b/tests/perf/PerformanceLaunch/Android.mk
@@ -22,4 +22,6 @@
 
 LOCAK_SDK_VERSION := current
 
+LOCAL_COMPATIBILITY_SUITE := device-tests
+
 include $(BUILD_PACKAGE)
diff --git a/tests/perf/PerformanceLaunch/AndroidTest.xml b/tests/perf/PerformanceLaunch/AndroidTest.xml
new file mode 100644
index 0000000..df61047
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/AndroidTest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Runs PerformanceLaunch tests.">
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+        <option name="test-file-name" value="PerformanceLaunch.apk" />
+    </target_preparer>
+
+    <option name="test-tag" value="PerformanceLaunch" />
+    <test class="com.android.tradefed.testtype.InstrumentationTest" >
+        <option name="package" value="com.android.performanceLaunch" />
+        <option name="runner" value="android.support.test.runner.AndroidJUnitRunner" />
+    </test>
+</configuration>
diff --git a/tests/perf/PerformanceLaunch/res/values-ar-rXB/strings.xml b/tests/perf/PerformanceLaunch/res/values-ar-rXB/strings.xml
deleted file mode 100644
index 8f90e7c..0000000
--- a/tests/perf/PerformanceLaunch/res/values-ar-rXB/strings.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources>
-    <string name="many_config_1">ManyConfig1-ar-rXB</string>
-    <string name="many_config_2">ManyConfig1-ar-rXB</string>
-    <string name="many_config_3">ManyConfig1-ar-rXB</string>
-    <string name="many_config_4">ManyConfig1-ar-rXB</string>
-</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-en-rXA/strings.xml b/tests/perf/PerformanceLaunch/res/values-en-rXA/strings.xml
deleted file mode 100644
index 67cf292..0000000
--- a/tests/perf/PerformanceLaunch/res/values-en-rXA/strings.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources>
-    <string name="many_config_1">ManyConfig1-en-rXA</string>
-    <string name="many_config_2">ManyConfig1-en-rXA</string>
-    <string name="many_config_3">ManyConfig1-en-rXA</string>
-    <string name="many_config_4">ManyConfig1-en-rXA</string>
-</resources>
\ No newline at end of file
diff --git a/tests/perf/PowerPerfTest/Android.mk b/tests/perf/PowerPerfTest/Android.mk
index 02a47e2..b3af570 100644
--- a/tests/perf/PowerPerfTest/Android.mk
+++ b/tests/perf/PowerPerfTest/Android.mk
@@ -22,4 +22,6 @@
 LOCAL_CERTIFICATE := platform
 LOCAL_SRC_FILES := $(call all-subdir-java-files)
 
+LOCAL_COMPATIBILITY_SUITE := device-tests
+
 include $(BUILD_PACKAGE)
diff --git a/tests/perf/PowerPerfTest/AndroidTest.xml b/tests/perf/PowerPerfTest/AndroidTest.xml
new file mode 100644
index 0000000..87ae903
--- /dev/null
+++ b/tests/perf/PowerPerfTest/AndroidTest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Runs Power Performance Tests.">
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+        <option name="test-file-name" value="PowerPerfTest.apk" />
+    </target_preparer>
+
+    <option name="test-tag" value="PowerPerfTest" />
+    <test class="com.android.tradefed.testtype.InstrumentationTest" >
+        <option name="package" value="com.android.powerperf.tests" />
+        <option name="runner" value="android.test.InstrumentationTestRunner" />
+    </test>
+</configuration>
diff --git a/utils/crashcollector/Android.mk b/utils/crashcollector/Android.mk
index 631ff61..cc8bdd0 100644
--- a/utils/crashcollector/Android.mk
+++ b/utils/crashcollector/Android.mk
@@ -21,6 +21,7 @@
 LOCAL_MODULE_PATH := $(TARGET_OUT_DATA)/local/tmp/crashcollector
 LOCAL_MODULE_TAGS := optional
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_COMPATIBILITY_SUITE := device-tests
 
 include $(BUILD_JAVA_LIBRARY)
 
diff --git a/utils/crashcollector/src/android/test/crashcollector/Collector.java b/utils/crashcollector/src/android/test/crashcollector/Collector.java
index 515b73b..87f2385 100644
--- a/utils/crashcollector/src/android/test/crashcollector/Collector.java
+++ b/utils/crashcollector/src/android/test/crashcollector/Collector.java
@@ -16,7 +16,6 @@
 
 package android.test.crashcollector;
 
-import android.app.ActivityManagerNative;
 import android.app.IActivityController;
 import android.app.IActivityManager;
 import android.content.Context;
@@ -84,7 +83,7 @@
         do {
             try {
                 // set activity controller
-                IActivityManager iam = ActivityManagerNative.asInterface(am);
+                IActivityManager iam = IActivityManager.Stub.asInterface(am);
                 iam.setActivityController(controller, false);
                 // register death recipient for activity manager
                 am.linkToDeath(death, 0);
diff --git a/utils/dpad/src/android/platform/test/utils/DPadUtil.java b/utils/dpad/src/android/platform/test/utils/DPadUtil.java
index 3e7f1d6..dd74f50 100644
--- a/utils/dpad/src/android/platform/test/utils/DPadUtil.java
+++ b/utils/dpad/src/android/platform/test/utils/DPadUtil.java
@@ -87,19 +87,46 @@
     }
 
     public boolean pressDPadLeft() {
-        return mDevice.pressDPadLeft();
+        return pressKeyCodeAndWait(KeyEvent.KEYCODE_DPAD_LEFT);
     }
 
     public boolean pressDPadRight() {
-        return mDevice.pressDPadRight();
+        return pressKeyCodeAndWait(KeyEvent.KEYCODE_DPAD_RIGHT);
     }
 
     public boolean pressDPadUp() {
-        return mDevice.pressDPadUp();
+        return pressKeyCodeAndWait(KeyEvent.KEYCODE_DPAD_UP);
     }
 
     public boolean pressDPadDown() {
-        return mDevice.pressDPadDown();
+        return pressKeyCodeAndWait(KeyEvent.KEYCODE_DPAD_DOWN);
+    }
+
+    public boolean pressDPadCenter() {
+        return pressKeyCodeAndWait(KeyEvent.KEYCODE_DPAD_CENTER);
+    }
+
+    public boolean pressEnter() {
+        return pressKeyCodeAndWait(KeyEvent.KEYCODE_ENTER);
+    }
+
+    public boolean pressPipKey() {
+        return pressKeyCodeAndWait(KeyEvent.KEYCODE_WINDOW);
+    }
+
+    public boolean pressSearch() {
+        return pressKeyCodeAndWait(KeyEvent.KEYCODE_SEARCH);
+    }
+
+    public boolean pressKeyCode(int keyCode) {
+        return pressKeyCodeAndWait(keyCode);
+    }
+    public boolean pressKeyCodeAndWait(int keyCode) {
+        boolean retVal = mDevice.pressKeyCode(keyCode);
+        // Dpad key presses will cause some UI change to occur.
+        // Wait for the accessibility event stream to become idle.
+        mDevice.waitForIdle();
+        return retVal;
     }
 
     public boolean pressHome() {
@@ -110,25 +137,10 @@
         return mDevice.pressBack();
     }
 
-    public boolean pressDPadCenter() {
-        return mDevice.pressDPadCenter();
-    }
-
-    public boolean pressEnter() {
-        return mDevice.pressEnter();
-    }
-
-    public boolean pressPipKey() {
-        return mDevice.pressKeyCode(KeyEvent.KEYCODE_WINDOW);
-    }
-
-    public boolean pressKeyCode(int keyCode) {
-        return mDevice.pressKeyCode(keyCode);
-    }
-
     public boolean longPressKeyCode(int keyCode) {
         try {
             mDevice.executeShellCommand(String.format("input keyevent --longpress %d", keyCode));
+            mDevice.waitForIdle();
             return true;
         } catch (IOException e) {
             // Ignore
@@ -139,14 +151,21 @@
 
     /**
      * Press the key code, and waits for the given condition to become true.
-     * @param condition
      * @param keyCode
+     * @param condition
+     * @param longpress
      * @param timeout
      * @param <R>
      * @return
      */
+    public <R> R pressKeyCodeAndWait(int keyCode, EventCondition<R> condition, boolean longpress,
+            long timeout) {
+        return mDevice.performActionAndWait(new KeyEventRunnable(keyCode, longpress), condition,
+                timeout);
+    }
+
     public <R> R pressKeyCodeAndWait(int keyCode, EventCondition<R> condition, long timeout) {
-        return mDevice.performActionAndWait(new KeyEventRunnable(keyCode), condition, timeout);
+        return pressKeyCodeAndWait(keyCode, condition, false, timeout);
     }
 
     public <R> R pressDPadCenterAndWait(EventCondition<R> condition, long timeout) {
@@ -161,12 +180,21 @@
 
     private class KeyEventRunnable implements Runnable {
         private int mKeyCode;
+        private boolean mLongPress = false;
         public KeyEventRunnable(int keyCode) {
             mKeyCode = keyCode;
         }
+        public KeyEventRunnable(int keyCode, boolean longpress) {
+            mKeyCode = keyCode;
+            mLongPress = longpress;
+        }
         @Override
         public void run() {
-            mDevice.pressKeyCode(mKeyCode);
+            if (mLongPress) {
+                longPressKeyCode(mKeyCode);
+            } else {
+                pressKeyCode(mKeyCode);
+            }
         }
     }
 }