fix inconsistent sub directory name for test list file locations
am: 865d98a720

Change-Id: I75445fa4ec24d61ea0b792907f3607199ee06249
diff --git a/build/tasks/continuous_instrumentation_metric_tests.mk b/build/tasks/continuous_instrumentation_metric_tests.mk
index a93649b..90cf9f8 100644
--- a/build/tasks/continuous_instrumentation_metric_tests.mk
+++ b/build/tasks/continuous_instrumentation_metric_tests.mk
@@ -24,16 +24,13 @@
 
 my_package_name := continuous_instrumentation_metric_tests
 
-my_package_zip :=
-
-ifneq ($(strip $(my_modules)),)
 include $(BUILD_SYSTEM)/tasks/tools/package-modules.mk
-name := $(TARGET_PRODUCT)-continuous_instrumentation_metric_tests-$(FILE_NAME_TAG)
-$(call dist-for-goals, continuous_instrumentation_metric_tests, $(my_package_zip):$(name).zip)
-endif
 
 .PHONY: continuous_instrumentation_metric_tests
 continuous_instrumentation_metric_tests : $(my_package_zip)
 
+name := $(TARGET_PRODUCT)-continuous_instrumentation_metric_tests-$(FILE_NAME_TAG)
+$(call dist-for-goals, continuous_instrumentation_metric_tests, $(my_package_zip):$(name).zip)
+
 # Also build this when you run "make tests".
 tests: continuous_instrumentation_metric_tests
diff --git a/build/tasks/tests/instrumentation_metric_test_list.mk b/build/tasks/tests/instrumentation_metric_test_list.mk
index fd79cb8..8b14aed 100644
--- a/build/tasks/tests/instrumentation_metric_test_list.mk
+++ b/build/tasks/tests/instrumentation_metric_test_list.mk
@@ -12,4 +12,9 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-instrumentation_metric_tests :=
+instrumentation_metric_tests := \
+    crashcollector \
+    DocumentsUIPerfTests \
+    DocumentsUIAppPerfTests \
+    perf-setup.sh \
+    SurfaceComposition
diff --git a/build/tasks/tests/instrumentation_test_list.mk b/build/tasks/tests/instrumentation_test_list.mk
index 8079cb6..9465728 100644
--- a/build/tasks/tests/instrumentation_test_list.mk
+++ b/build/tasks/tests/instrumentation_test_list.mk
@@ -13,15 +13,28 @@
 # limitations under the License.
 
 instrumentation_tests := \
+    crashcollector \
     ManagedProvisioningTests \
     FrameworksCoreTests \
     FrameworksServicesTests \
     FrameworksUtilsTests \
+    MtpDocumentsProviderTests \
     DocumentsUITests \
+    ShellTests \
     SystemUITests \
     RecyclerViewTests \
+    FrameworksWifiTests \
+    FrameworksTelephonyTests \
     ContactsProviderTests \
-    AndroidVCardTests \
+    SettingsUnitTests \
     TelecomUnitTests \
+    AndroidVCardTests \
+    PermissionFunctionalTests \
+    BlockedNumberProviderTest \
+    SettingsFunctionalTests \
+    LauncherFunctionalTests \
+    DownloadAppFunctionalTests \
+    NotificationFunctionalTests \
     DownloadProviderTests \
+    EmergencyInfoTests \
     CalendarProviderTests
diff --git a/build/tasks/tests/native_metric_test_list.mk b/build/tasks/tests/native_metric_test_list.mk
index 310ab50..1ca9567 100644
--- a/build/tasks/tests/native_metric_test_list.mk
+++ b/build/tasks/tests/native_metric_test_list.mk
@@ -15,5 +15,7 @@
 native_metric_tests := \
     binderAddInts \
     bionic-benchmarks \
+    crashcollector \
     libjavacore-benchmarks \
-    mmapPerf
+    mmapPerf \
+    perf-setup.sh
diff --git a/build/tasks/tests/native_test_list.mk b/build/tasks/tests/native_test_list.mk
index 739704c..10f5d2e 100644
--- a/build/tasks/tests/native_test_list.mk
+++ b/build/tasks/tests/native_test_list.mk
@@ -19,6 +19,7 @@
     bluetoothtbd_test \
     camera2_test \
     camera_client_test \
+    crashcollector \
     debuggerd_test \
     hwui_unit_tests \
     init_tests \
@@ -36,11 +37,14 @@
     malloc_debug_unit_tests \
     memory_replay_tests \
     minadbd_test \
+    minikin_tests \
     net_test_bluetooth \
     net_test_btcore \
     net_test_device \
     net_test_hci \
     net_test_osi \
+    netd_integration_test \
+    netd_unit_test \
     pagemap_test \
     perfprofd_test \
     simpleperf_cpu_hotplug_test \
diff --git a/libraries/annotations/Android.mk b/libraries/annotations/Android.mk
new file mode 100644
index 0000000..9c4c2e1
--- /dev/null
+++ b/libraries/annotations/Android.mk
@@ -0,0 +1,28 @@
+#
+# 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.
+#
+LOCAL_PATH := $(call my-dir)
+
+# Build for device side tests
+include $(CLEAR_VARS)
+LOCAL_MODULE := platform-test-annotations
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
+# Build for host side tests
+include $(CLEAR_VARS)
+LOCAL_MODULE := platform-test-annotations-host
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+include $(BUILD_HOST_JAVA_LIBRARY)
diff --git a/libraries/annotations/src/android/platform/test/annotations/ApiTest.java b/libraries/annotations/src/android/platform/test/annotations/ApiTest.java
new file mode 100644
index 0000000..e3cd1f7
--- /dev/null
+++ b/libraries/annotations/src/android/platform/test/annotations/ApiTest.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 the type of test with purpose of asserting API functionalities and behaviors.
+ *
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.TYPE})
+public @interface ApiTest {
+
+}
diff --git a/libraries/annotations/src/android/platform/test/annotations/Postsubmit.java b/libraries/annotations/src/android/platform/test/annotations/Postsubmit.java
new file mode 100644
index 0000000..a435159
--- /dev/null
+++ b/libraries/annotations/src/android/platform/test/annotations/Postsubmit.java
@@ -0,0 +1,37 @@
+/*
+ * 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 postsubmit suite for platform development. By logic,
+ * all presubmit tests should be automatically included in postsubmit testing. This annotation is
+ * intended for additional tests that asserts behaviors of higher level of complexity and/or with
+ * deeper dependencies into the system.
+ * <p>
+ * @see Presubmit
+ *
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.TYPE})
+public @interface Postsubmit {
+
+}
diff --git a/libraries/annotations/src/android/platform/test/annotations/Presubmit.java b/libraries/annotations/src/android/platform/test/annotations/Presubmit.java
new file mode 100644
index 0000000..5f08acc
--- /dev/null
+++ b/libraries/annotations/src/android/platform/test/annotations/Presubmit.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 presubmit suite for platform development.
+ *
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.TYPE})
+public @interface Presubmit {
+
+}
diff --git a/libraries/annotations/src/android/platform/test/annotations/QualityTest.java b/libraries/annotations/src/android/platform/test/annotations/QualityTest.java
new file mode 100644
index 0000000..a02f902
--- /dev/null
+++ b/libraries/annotations/src/android/platform/test/annotations/QualityTest.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 the type of test with purpose of evaluating qualities such as performance and stability.
+ *
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.TYPE})
+public @interface QualityTest {
+
+}
diff --git a/libraries/annotations/src/android/platform/test/annotations/RestrictedBuildTest.java b/libraries/annotations/src/android/platform/test/annotations/RestrictedBuildTest.java
new file mode 100644
index 0000000..adb4147
--- /dev/null
+++ b/libraries/annotations/src/android/platform/test/annotations/RestrictedBuildTest.java
@@ -0,0 +1,36 @@
+/*
+ * 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 as only suitable for execution on restricted builds. Such builds are typically
+ * user builds for release to end users, as opposed to userdebug or eng builds meant for
+ * development. Tests that only passes on restricted builds typically assert on tightened security
+ * restrictions not implemented on userdebug/eng builds, therefore it's important to distinguish
+ * with this annotation.
+ *
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.TYPE})
+public @interface RestrictedBuildTest {
+
+}
diff --git a/libraries/annotations/src/android/platform/test/annotations/RootPermissionTest.java b/libraries/annotations/src/android/platform/test/annotations/RootPermissionTest.java
new file mode 100644
index 0000000..03a964d
--- /dev/null
+++ b/libraries/annotations/src/android/platform/test/annotations/RootPermissionTest.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;
+
+/**
+ * Marks host test that performs actions requiring root permission, or device test that requires
+ * certain pre-test setup steps only possible with root permission.
+ *
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.TYPE})
+public @interface RootPermissionTest {
+
+}
diff --git a/libraries/annotations/src/android/platform/test/annotations/SecurityTest.java b/libraries/annotations/src/android/platform/test/annotations/SecurityTest.java
new file mode 100644
index 0000000..8d07d4e
--- /dev/null
+++ b/libraries/annotations/src/android/platform/test/annotations/SecurityTest.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 the type of test with purpose of evaluating security vulnerabilities.
+ *
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.TYPE})
+public @interface SecurityTest {
+
+}
diff --git a/libraries/app-helpers/Android.mk b/libraries/app-helpers/Android.mk
new file mode 100644
index 0000000..25d4913
--- /dev/null
+++ b/libraries/app-helpers/Android.mk
@@ -0,0 +1,39 @@
+#
+# 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.
+#
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := app-helpers
+LOCAL_STATIC_JAVA_LIBRARIES := launcher-helper-lib base-app-helpers google-camera-app-helper \
+                               youtube-app-helper photos-app-helper play-music-app-helper \
+                               chrome-app-helper play-store-app-helper play-movies-app-helper \
+                               gmail-app-helper maps-app-helper recents-app-helper \
+                               facebook-app-helper google-keyboard-app-helper \
+                               google-messenger-app-helper reddit-app-helper \
+                               play-books-app-helper tunein-app-helper \
+                               google-docs-app-helper flightdemo-app-helper
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
+######################################
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := leanback-app-helpers
+LOCAL_STATIC_JAVA_LIBRARIES := launcher-helper-lib base-app-helpers
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/aupt-lib/AndroidManifest.xml b/libraries/aupt-lib/AndroidManifest.xml
index d7560a7..8e0f65c 100644
--- a/libraries/aupt-lib/AndroidManifest.xml
+++ b/libraries/aupt-lib/AndroidManifest.xml
@@ -18,7 +18,7 @@
     <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.GET_ACCOUNTS" />
+    <uses-permission android:name="android.permission.WAKE_LOCK" />
     <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 964ccfc..d7f95b6 100644
--- a/libraries/aupt-lib/src/android/support/test/aupt/AuptTestCase.java
+++ b/libraries/aupt-lib/src/android/support/test/aupt/AuptTestCase.java
@@ -35,6 +35,9 @@
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+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;
@@ -49,6 +52,7 @@
 import android.test.InstrumentationTestRunner;
 import android.util.Log;
 
+import junit.framework.Assert;
 
 /**
  * Base class for AuptTests.
@@ -197,7 +201,7 @@
 
         mDevice = UiDevice.getInstance(getInstrumentation());
         mWatchers = new UiWatchers();
-        mWatchers.registerAnrAndCrashWatchers();
+        mWatchers.registerAnrAndCrashWatchers(getInstrumentation());
         mDevice.registerWatcher("LockScreenWatcher", new LockScreenWatcher());
         mRecordMeminfo = "true".equals(getParams().getString(RECORD_MEMINFO_PARAM, "false"));
 
@@ -550,4 +554,21 @@
 
         return version;
     }
+
+    /**
+     * Get registered accounts
+     * Ensures there is at least one account registered
+     * returns the google account name
+     */
+    public String getRegisteredEmailAccount() {
+        Account[] accounts = AccountManager.get(getInstrumentation().getContext()).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");
+    }
 }
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 c91cfd7..75030bf 100644
--- a/libraries/aupt-lib/src/android/support/test/aupt/AuptTestRunner.java
+++ b/libraries/aupt-lib/src/android/support/test/aupt/AuptTestRunner.java
@@ -43,7 +43,9 @@
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.concurrent.TimeUnit;
 import java.util.HashMap;
 import java.util.List;
@@ -57,9 +59,9 @@
  * collecting bugreports and procrank data while the test is running.
  */
 public class AuptTestRunner extends InstrumentationTestRunner {
-
-    private static final String TAG = "AuptTestRunner";
     private static final String DEFAULT_JAR_PATH = "/data/local/tmp/";
+
+    private static final String LOG_TAG = "AuptTestRunner";
     private static final String DEX_OPT_PATH = "aupt-opt";
     private static final String PARAM_JARS = "jars";
     private Bundle mParams;
@@ -71,6 +73,9 @@
     private DataCollector mDataCollector;
     private File mResultsDirectory;
 
+    private boolean mDeleteOldFiles;
+    private long mFileRetainCount;
+
     private AuptPrivateTestRunner mRunner = new AuptPrivateTestRunner();
     private ClassLoader mLoader = null;
     private Context mTargetContextWrapper;
@@ -122,13 +127,20 @@
                     GraphicsStatsMonitor.DEFAULT_INTERVAL_RATE);
             mGraphicsStatsMonitor.setIntervalRate(interval);
         }
-
         mRunner.addTestListener(new PidChecker());
         mResultsDirectory = new File(Environment.getExternalStorageDirectory(),
                 parseStringParam("outputLocation", "aupt_results"));
         if (!mResultsDirectory.exists() && !mResultsDirectory.mkdirs()) {
-            Log.w(TAG, "Did not create output directory");
+            Log.w(LOG_TAG, "Did not create output directory");
         }
+
+        mFileRetainCount = parseLongParam("fileRetainCount", -1);
+        if (mFileRetainCount == -1) {
+            mDeleteOldFiles = false;
+        } else {
+            mDeleteOldFiles = true;
+        }
+
         mDataCollector = new DataCollector(
                 TimeUnit.MINUTES.toMillis(parseLongParam("bugreportInterval", 0)),
                 TimeUnit.MINUTES.toMillis(parseLongParam("meminfoInterval", 0)),
@@ -136,6 +148,7 @@
                 TimeUnit.MINUTES.toMillis(parseLongParam("fragmentationInterval", 0)),
                 TimeUnit.MINUTES.toMillis(parseLongParam("ionInterval", 0)),
                 TimeUnit.MINUTES.toMillis(parseLongParam("pagetypeinfoInterval", 0)),
+                TimeUnit.MINUTES.toMillis(parseLongParam("traceInterval", 0)),
                 mResultsDirectory, this);
         String jars = params.getString(PARAM_JARS);
         if (jars != null) {
@@ -217,7 +230,7 @@
             fos.flush();
             fos.close();
         } catch (IOException ioe) {
-            Log.e(TAG, "error saving progress file", ioe);
+            Log.e(LOG_TAG, "error saving progress file", ioe);
         }
     }
 
@@ -270,7 +283,7 @@
             List<JankStat> mergedStats = mGraphicsStatsMonitor.aggregateStatsImages();
             String mergedStatsString = JankStat.statsListToString(mergedStats);
 
-            Log.d(TAG, "Writing jank metrics to the graphics file");
+            Log.d(LOG_TAG, "Writing jank metrics to the graphics file");
             writeGraphicsMessage(mergedStatsString);
         }
     }
@@ -329,14 +342,14 @@
                     // but trigger a service ANR first
                     if (mGenerateAnr) {
                         Context ctx = getTargetContext();
-                        Log.d(TAG, "About to induce artificial ANR for debugging");
+                        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(TAG, "interrupted in wait on BadService");
+                            Log.d(LOG_TAG, "interrupted in wait on BadService");
                             return;
                         }
                     } else {
@@ -352,6 +365,10 @@
                 for (TestCase testCase : mTestCases) {
                     setInstrumentationIfInstrumentationTestCase(testCase, mInstrumentation);
                     setupAuptIfAuptTestCase(testCase);
+
+                    // Remove device storage as necessary
+                    removeOldImagesFromDcimCameraFolder();
+
                     Thread timeBombThread = null;
                     if (mTestCaseTimeout > 0) {
                         timeBombThread = new Thread(timeBomb);
@@ -422,6 +439,32 @@
             }
         }
 
+        private void removeOldImagesFromDcimCameraFolder() {
+            if (!mDeleteOldFiles) {
+                return;
+            }
+
+            File dcimFolder = new File(Environment.getExternalStorageDirectory(), "DCIM");
+            if (dcimFolder != null) {
+                File cameraFolder = new File(dcimFolder, "Camera");
+                if (cameraFolder != null) {
+                    File[] allMediaFiles = cameraFolder.listFiles();
+                    Arrays.sort(allMediaFiles, new Comparator<File> () {
+                        public int compare(File f1, File f2) {
+                            return Long.valueOf(f1.lastModified()).compareTo(f2.lastModified());
+                        }
+                    });
+                    for (int i = 0; i < allMediaFiles.length - mFileRetainCount; i++) {
+                        allMediaFiles[i].delete();
+                    }
+                } else {
+                    Log.w(LOG_TAG, "No Camera folder found to delete from.");
+                }
+            } else {
+                Log.w(LOG_TAG, "No DCIM folder found to delete from.");
+            }
+        }
+
         @Override
         public void clearTestListeners() {
             mTestListeners.clear();
@@ -458,7 +501,7 @@
 
         @Override
         public void addError(Test test, Throwable t) {
-            Log.e(TAG, "Caught exception from a test", t);
+            Log.e(LOG_TAG, "Caught exception from a test", t);
             if ((t instanceof AuptTerminator)) {
                 throw (AuptTerminator)t;
             } else {
@@ -468,12 +511,12 @@
                 }
                 // if previous line did not throw an exception, we are interested to know what
                 // caused the UI exception
-                Log.v(TAG, "Dumping UI 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(TAG, "Failed to create UI hierarchy dump for UI error", e);
+                    Log.w(LOG_TAG, "Failed to create UI hierarchy dump for UI error", e);
                 }
             }
 
@@ -596,9 +639,9 @@
 
         @Override
         public int onStartCommand(Intent intent, int flags, int id) {
-            Log.i(TAG, "in service start -- about to hang");
-            try { Thread.sleep(DELAY); } catch (InterruptedException e) { Log.wtf(TAG, e); }
-            Log.i(TAG, "service hang finished -- stopping and returning");
+            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;
         }
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 0ff1636..bec2daa 100644
--- a/libraries/aupt-lib/src/android/support/test/aupt/DataCollector.java
+++ b/libraries/aupt-lib/src/android/support/test/aupt/DataCollector.java
@@ -36,7 +36,7 @@
 public class DataCollector {
     private static final String TAG = "AuptDataCollector";
     private long mBugreportInterval, mMeminfoInterval, mCpuinfoInterval, mFragmentationInterval,
-            mIonHeapInterval, mPageTypeInfoInterval;
+            mIonHeapInterval, mPageTypeInfoInterval, mTraceInterval;
     private File mResultsDirectory;
 
     private Thread mLoggerThread;
@@ -45,7 +45,7 @@
 
     public DataCollector(long bugreportInterval, long meminfoInterval, long cpuinfoInterval,
             long fragmentationInterval, long ionHeapInterval, long pagetypeinfoInterval,
-            File outputLocation, Instrumentation intrumentation) {
+            long traceInterval, File outputLocation, Instrumentation intrumentation) {
         mBugreportInterval = bugreportInterval;
         mMeminfoInterval = meminfoInterval;
         mCpuinfoInterval = cpuinfoInterval;
@@ -53,6 +53,7 @@
         mIonHeapInterval = ionHeapInterval;
         mPageTypeInfoInterval = pagetypeinfoInterval;
         mResultsDirectory = outputLocation;
+        mTraceInterval = traceInterval;
         mInstrumentation = intrumentation;
     }
 
@@ -74,11 +75,12 @@
     private class Logger implements Runnable {
         private final long mIntervals[] = {
                 mBugreportInterval, mMeminfoInterval, mCpuinfoInterval, mFragmentationInterval,
-                mIonHeapInterval, mPageTypeInfoInterval
+                mIonHeapInterval, mPageTypeInfoInterval, mTraceInterval
         };
         private final LogGenerator mLoggers[] = {
                 new BugreportGenerator(), new CompactMemInfoGenerator(), new CpuInfoGenerator(),
-                new FragmentationGenerator(), new IonHeapGenerator(), new PageTypeInfoGenerator()
+                new FragmentationGenerator(), new IonHeapGenerator(), new PageTypeInfoGenerator(),
+                new TraceGenerator()
         };
 
         private final long mLastUpdate[] = new long[mLoggers.length];
@@ -226,6 +228,17 @@
         }
     }
 
+    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);
@@ -251,6 +264,11 @@
         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();
diff --git a/libraries/aupt-lib/src/android/support/test/aupt/GraphicsStatsMonitor.java b/libraries/aupt-lib/src/android/support/test/aupt/GraphicsStatsMonitor.java
index ec5f259..ba11c75 100644
--- a/libraries/aupt-lib/src/android/support/test/aupt/GraphicsStatsMonitor.java
+++ b/libraries/aupt-lib/src/android/support/test/aupt/GraphicsStatsMonitor.java
@@ -67,7 +67,9 @@
         mIntervalTask = new TimerTask() {
             @Override
             public void run () {
-                grabStatsImage();
+                if (mIsRunning) {
+                    grabStatsImage();
+                }
             }
         };
         mIntervalRate = DEFAULT_INTERVAL_RATE;
@@ -205,45 +207,61 @@
             while ((line = stream.readLine()) != null) {
                 String proc = JankStat.StatPattern.PACKAGE.parse(line);
                 if (proc != null) {
-                    // Line 1 = "Package: a.b.c"
+                    // "Package: a.b.c"
                     Log.v(TAG, String.format("Found process, %s. Gathering jank info.", proc));
-                    // Line 2 = "Stats since: ###ns"
+                    // "Stats since: ###ns"
                     line = stream.readLine();
                     Long since = Long.parseLong(JankStat.StatPattern.STATS_SINCE.parse(line));
-                    // Line 3 = "Total frames rendered: ###"
+                    // "Total frames rendered: ###"
                     line = stream.readLine();
                     int total = Integer.valueOf(JankStat.StatPattern.TOTAL_FRAMES.parse(line));
-                    // Line 4 = "Janky frames: ## (#.#%)"
-                    //       OR "Janky frames: ## (nan%)"
+                    // "Janky frames: ## (#.#%)" OR
+                    // "Janky frames: ## (nan%)"
                     line = stream.readLine();
                     int janky = Integer.valueOf(JankStat.StatPattern.NUM_JANKY.parse(line));
-                    // Line 5 = "90th percentile: ##ms"
+                    // (optional, N+) "50th percentile: ##ms"
                     line = stream.readLine();
+                    int perc50;
+                    String parsed50 = JankStat.StatPattern.FRAME_TIME_50TH.parse(line);
+                    if (parsed50 != null || !parsed50.isEmpty()) {
+                        perc50 = Integer.valueOf(parsed50);
+                        line = stream.readLine();
+                    } else {
+                        perc50 = -1;
+                    }
+                    // "90th percentile: ##ms"
                     int perc90 = Integer.valueOf(JankStat.StatPattern.FRAME_TIME_90TH.parse(line));
-                    // Line 6 = "95th percentile: ##ms"
+                    // "95th percentile: ##ms"
                     line = stream.readLine();
                     int perc95 = Integer.valueOf(JankStat.StatPattern.FRAME_TIME_95TH.parse(line));
-                    // Line 7 = "99th percentile: ##ms"
+                    // "99th percentile: ##ms"
                     line = stream.readLine();
                     int perc99 = Integer.valueOf(JankStat.StatPattern.FRAME_TIME_99TH.parse(line));
-                    // Line 8 = "Number Missed Vsync: #"
+                    // "Slowest frames last 24h: ##ms ##ms ..."
                     line = stream.readLine();
+                    String slowest = JankStat.StatPattern.SLOWEST_FRAMES_24H.parse(line);
+                    if (slowest != null && !slowest.isEmpty()) {
+                        line = stream.readLine();
+                    }
+                    // "Number Missed Vsync: #"
                     int vsync = Integer.valueOf(JankStat.StatPattern.NUM_MISSED_VSYNC.parse(line));
-                    // Line 9 = "Number High input latency: #"
+                    // "Number High input latency: #"
                     line = stream.readLine();
-                    int latency = Integer.valueOf(JankStat.StatPattern.NUM_HIGH_INPUT_LATENCY.parse(line));
-                    // Line 10 = "Number slow UI thread: #"
+                    int latency = Integer.valueOf(
+                            JankStat.StatPattern.NUM_HIGH_INPUT_LATENCY.parse(line));
+                    // "Number slow UI thread: #"
                     line = stream.readLine();
                     int ui = Integer.valueOf(JankStat.StatPattern.NUM_SLOW_UI_THREAD.parse(line));
-                    // Line 11 = "Number Slow bitmap uploads: #"
+                    // "Number Slow bitmap uploads: #"
                     line = stream.readLine();
-                    int bmp = Integer.valueOf(JankStat.StatPattern.NUM_SLOW_BITMAP_UPLOADS.parse(line));
-                    // Line 12 = "Number slow issue draw commands: #"
+                    int bmp = Integer.valueOf(
+                            JankStat.StatPattern.NUM_SLOW_BITMAP_UPLOADS.parse(line));
+                    // "Number slow issue draw commands: #"
                     line = stream.readLine();
                     int draw = Integer.valueOf(JankStat.StatPattern.NUM_SLOW_DRAW.parse(line));
 
-                    JankStat stat = new JankStat(proc, since, total, janky, perc90, perc95, perc99,
-                            vsync, latency, ui, bmp, draw, 1);
+                    JankStat stat = new JankStat(proc, since, total, janky, perc50, perc90, perc95,
+                            perc99, slowest, vsync, latency, ui, bmp, draw, 1);
                     result.add(stat);
                 }
             }
diff --git a/libraries/aupt-lib/src/android/support/test/aupt/JankStat.java b/libraries/aupt-lib/src/android/support/test/aupt/JankStat.java
index b0d5d50..8c555be 100644
--- a/libraries/aupt-lib/src/android/support/test/aupt/JankStat.java
+++ b/libraries/aupt-lib/src/android/support/test/aupt/JankStat.java
@@ -38,9 +38,11 @@
         STATS_SINCE(Pattern.compile("\\s*Stats since: (\\d+)ns"), 1),
         TOTAL_FRAMES(Pattern.compile("\\s*Total frames rendered: (\\d+)"), 1),
         NUM_JANKY(Pattern.compile("\\s*Janky frames: (\\d+) (.*)"), 1),
+        FRAME_TIME_50TH(Pattern.compile("\\s*50th percentile: (\\d+)ms"), 1),
         FRAME_TIME_90TH(Pattern.compile("\\s*90th percentile: (\\d+)ms"), 1),
         FRAME_TIME_95TH(Pattern.compile("\\s*95th percentile: (\\d+)ms"), 1),
         FRAME_TIME_99TH(Pattern.compile("\\s*99th percentile: (\\d+)ms"), 1),
+        SLOWEST_FRAMES_24H(Pattern.compile("\\s*Slowest frames over last 24h: (.*)"), 1),
         NUM_MISSED_VSYNC(Pattern.compile("\\s*Number Missed Vsync: (\\d+)"), 1),
         NUM_HIGH_INPUT_LATENCY(Pattern.compile("\\s*Number High input latency: (\\d+)"), 1),
         NUM_SLOW_UI_THREAD(Pattern.compile("\\s*Number Slow UI thread: (\\d+)"), 1),
@@ -69,9 +71,11 @@
     public long statsSince;
     public int totalFrames;
     public int jankyFrames;
+    public int frameTime50th;
     public int frameTime90th;
     public int frameTime95th;
     public int frameTime99th;
+    public String slowestFrames24h;
     public int numMissedVsync;
     public int numHighLatency;
     public int numSlowUiThread;
@@ -80,16 +84,18 @@
 
     public int aggregateCount;
 
-    public JankStat (String pkg, long since, int total, int janky, int ft90, int ft95,
-            int ft99, int vsync, int latency, int slowUi, int slowBmp, int slowDraw,
+    public JankStat (String pkg, long since, int total, int janky, int ft50, int ft90, int ft95,
+            int ft99, String slow24h, 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;
+        slowestFrames24h = slow24h;
         numMissedVsync = vsync;
         numHighLatency = latency;
         numSlowUiThread = slowUi;
@@ -125,9 +131,11 @@
                 "\nStats since: " + statsSince +
                 "\nTotal frames: " + totalFrames +
                 "\nJanky frames: " + jankyFrames +
+                "\n50th percentile: " + frameTime50th +
                 "\n90th percentile: " + frameTime90th +
                 "\n95th percentile: " + frameTime95th +
                 "\n99th percentile: " + frameTime99th +
+                "\nSlowest frames over last 24h: " + slowestFrames24h +
                 "\nNumber Missed Vsync: " + numMissedVsync +
                 "\nNumber High input latency: " + numHighLatency +
                 "\nNumber Slow UI thread: " + numSlowUiThread +
@@ -145,7 +153,9 @@
      *     ## = 90, 95, and 99
      */
     public static JankStat mergeStatHistory (List<JankStat> statHistory) {
-        if (statHistory.size() == 1)
+        if (statHistory.size() == 0)
+            return null;
+        else if (statHistory.size() == 1)
             return statHistory.get(0);
 
         String pkg = statHistory.get(0).packageName;
@@ -157,6 +167,7 @@
         int totalNumSlowUiThread = 0;
         int totalNumSlowBitmap = 0;
         int totalNumSlowDraw = 0;
+        String totalSlow24h = "";
 
         for (JankStat stat : statHistory) {
             totalTotalFrames += stat.totalFrames;
@@ -166,26 +177,30 @@
             totalNumSlowUiThread += stat.numSlowUiThread;
             totalNumSlowBitmap += stat.numSlowBitmap;
             totalNumSlowDraw += stat.numSlowDraw;
+            totalSlow24h += stat.slowestFrames24h;
         }
 
+        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));
+            wgtAvgPercentile90 += 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, perc90, perc95, perc99, totalNumMissedVsync,
-                totalNumHighLatency, totalNumSlowUiThread, totalNumSlowBitmap,
+                totalJankyFrames, perc50, perc90, perc95, perc99, totalSlow24h,
+                totalNumMissedVsync, totalNumHighLatency, totalNumSlowUiThread, totalNumSlowBitmap,
                 totalNumSlowDraw, statHistory.size());
     }
 
diff --git a/libraries/aupt-lib/src/android/support/test/aupt/ProcessStatusTracker.java b/libraries/aupt-lib/src/android/support/test/aupt/ProcessStatusTracker.java
index 6ba1a59..d7ddd13 100644
--- a/libraries/aupt-lib/src/android/support/test/aupt/ProcessStatusTracker.java
+++ b/libraries/aupt-lib/src/android/support/test/aupt/ProcessStatusTracker.java
@@ -139,6 +139,7 @@
             mPidExclusions.remove(processName);
             Log.v(TAG, "Started tracking pid changes: " + processName);
         }
+        verifyRunningProcess();
     }
 
     @Override
@@ -186,7 +187,7 @@
         // Enumerate status for all currently tracked processes
         for (String proc : procSet) {
             // Execute shell command and parse results
-            BufferedReader stream = executeShellCommand(String.format("ps %s", proc));
+            BufferedReader stream = executeShellCommand("ps");
             try {
                 String line;
                 while ((line = stream.readLine()) != null) {
diff --git a/libraries/aupt-lib/src/android/support/test/aupt/UiWatchers.java b/libraries/aupt-lib/src/android/support/test/aupt/UiWatchers.java
index 41e89d1..a8483da 100644
--- a/libraries/aupt-lib/src/android/support/test/aupt/UiWatchers.java
+++ b/libraries/aupt-lib/src/android/support/test/aupt/UiWatchers.java
@@ -16,13 +16,13 @@
 
 package android.support.test.aupt;
 
-import android.util.Log;
-
+import android.app.Instrumentation;
+import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.UiDevice;
-import android.support.test.uiautomator.UiObject;
-import android.support.test.uiautomator.UiObjectNotFoundException;
-import android.support.test.uiautomator.UiSelector;
+import android.support.test.uiautomator.UiObject2;
 import android.support.test.uiautomator.UiWatcher;
+import android.support.test.uiautomator.Until;
+import android.util.Log;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -39,83 +39,34 @@
      * This is a sample watcher looking for ANR and crashes. it closes it and moves on. You should
      * create your own watchers and handle error logging properly for your type of tests.
      */
-    public void registerAnrAndCrashWatchers() {
-
-        UiDevice.getInstance().registerWatcher("ANR", new UiWatcher() {
-            @Override
-            public boolean checkForCondition() {
-                UiObject window = new UiObject(new UiSelector().className(
-                        "com.android.server.am.AppNotRespondingDialog"));
-                String errorText = null;
-                if (window.exists()) {
-                    try {
-                        errorText = window.getText();
-                    } catch (UiObjectNotFoundException e) {
-                        Log.e(LOG_TAG, "dialog gone?", e);
-                    }
-                    onAnrDetected(errorText);
-                    postHandler();
-                    return true; // triggered
-                }
-                return false; // no trigger
-            }
-        });
+    public void registerAnrAndCrashWatchers(Instrumentation instr) {
+        final UiDevice device = UiDevice.getInstance(instr);
 
         // class names may have changed
-        UiDevice.getInstance().registerWatcher("ANR2", new UiWatcher() {
+        device.registerWatcher("AnrWatcher", new UiWatcher() {
             @Override
             public boolean checkForCondition() {
-                UiObject window = new UiObject(new UiSelector().packageName("android")
-                        .textContains("isn't responding."));
-                if (window.exists()) {
-                    String errorText = null;
-                    try {
-                        errorText = window.getText();
-                    } catch (UiObjectNotFoundException e) {
-                        Log.e(LOG_TAG, "dialog gone?", e);
-                    }
+                UiObject2 window = device.findObject(
+                        By.pkg("android").textContains("isn't responding"));
+                if (window != null) {
+                    String errorText = window.getText();
                     onAnrDetected(errorText);
-                    postHandler();
+                    postHandler(device);
                     return true; // triggered
                 }
                 return false; // no trigger
             }
         });
 
-        UiDevice.getInstance().registerWatcher("CRASH", new UiWatcher() {
+        device.registerWatcher("CrashWatcher", new UiWatcher() {
             @Override
             public boolean checkForCondition() {
-                UiObject window = new UiObject(new UiSelector().className(
-                        "com.android.server.am.AppErrorDialog"));
-                if (window.exists()) {
-                    String errorText = null;
-                    try {
-                        errorText = window.getText();
-                    } catch (UiObjectNotFoundException e) {
-                        Log.e(LOG_TAG, "dialog gone?", e);
-                    }
+                UiObject2 window = device.findObject(
+                        By.pkg("android").textContains("has stopped"));
+                if (window != null) {
+                    String errorText = window.getText();
                     onCrashDetected(errorText);
-                    postHandler();
-                    return true; // triggered
-                }
-                return false; // no trigger
-            }
-        });
-
-        UiDevice.getInstance().registerWatcher("CRASH2", new UiWatcher() {
-            @Override
-            public boolean checkForCondition() {
-                UiObject window = new UiObject(new UiSelector().packageName("android")
-                        .textContains("has stopped"));
-                if (window.exists()) {
-                    String errorText = null;
-                    try {
-                        errorText = window.getText();
-                    } catch (UiObjectNotFoundException e) {
-                        Log.e(LOG_TAG, "dialog gone?", e);
-                    }
-                    onCrashDetected(errorText);
-                    postHandler();
+                    postHandler(device);
                     return true; // triggered
                 }
                 return false; // no trigger
@@ -125,6 +76,12 @@
         Log.i(LOG_TAG, "Registed GUI Exception watchers");
     }
 
+    public void removeAnrAndCrashWatchers(Instrumentation instr) {
+        final UiDevice device = UiDevice.getInstance(instr);
+        device.removeWatcher("AnrWatcher");
+        device.removeWatcher("CrashWatcher");
+    }
+
     public void onAnrDetected(String errorText) {
         mErrors.add(errorText);
     }
@@ -144,20 +101,27 @@
     /**
      * Current implementation ignores the exception and continues.
      */
-    public void postHandler() {
+    public void postHandler(UiDevice device) {
         // TODO: Add custom error logging here
 
-        String formatedOutput = String.format("UI Exception Message: %-20s\n", UiDevice
-                .getInstance().getCurrentPackageName());
+        String formatedOutput = String.format("UI Exception Message: %-20s\n",
+                device.getCurrentPackageName());
         Log.e(LOG_TAG, formatedOutput);
 
-        UiObject buttonOK = new UiObject(new UiSelector().text("OK").enabled(true));
+        UiObject2 buttonMute = device.findObject(By.res("android", "aerr_mute"));
+        if (buttonMute != null) {
+            buttonMute.click();
+        }
+
+        UiObject2 closeAppButton = device.findObject(By.res("android", "aerr_close"));
+        if (closeAppButton != null) {
+            closeAppButton.click();
+        }
+
         // sometimes it takes a while for the OK button to become enabled
-        buttonOK.waitForExists(5000);
-        try {
+        UiObject2 buttonOK = device.findObject(By.text("OK").enabled(true));
+        if (buttonOK != null) {
             buttonOK.click();
-        } catch (UiObjectNotFoundException e) {
-            Log.e(LOG_TAG, "Exception", e);
         }
     }
 }
diff --git a/libraries/base-app-helpers/Android.mk b/libraries/base-app-helpers/Android.mk
new file mode 100644
index 0000000..c46cc84
--- /dev/null
+++ b/libraries/base-app-helpers/Android.mk
@@ -0,0 +1,23 @@
+#
+# 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.
+#
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := base-app-helpers
+LOCAL_JAVA_LIBRARIES := ub-uiautomator launcher-helper-lib
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractChromeHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractChromeHelper.java
new file mode 100644
index 0000000..050a1c9
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractChromeHelper.java
@@ -0,0 +1,64 @@
+/*
+ * 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 AbstractChromeHelper extends AbstractStandardAppHelper {
+
+    public AbstractChromeHelper(Instrumentation instr) {
+        super(instr);
+    }
+
+    /**
+     * Setup expectations: Chrome is open and on a standard page, i.e. a tab is open.
+     *
+     * This method will open the URL supplied and block until the page is open.
+     */
+    public abstract void openUrl(String url);
+
+    /**
+     * Setup expectations: Chrome is open on a page.
+     *
+     * This method will scroll the page as directed and block until idle.
+     */
+    public abstract void flingPage(Direction dir);
+
+    /**
+     * Setup expectations: Chrome is open on a page.
+     *
+     * This method will open the overload menu, indicated by three dots and block until open.
+     */
+    public abstract void openMenu();
+
+    /**
+     * Setup expectations: Chrome is open on a page and the tabs are treated as apps.
+     *
+     * This method will change the settings to treat tabs inside of Chrome and block until Chrome is
+     * open on the original tab.
+     */
+    public abstract void mergeTabs();
+
+    /**
+     * Setup expectations: Chrome is open on a page and the tabs are merged.
+     *
+     * This method will change the settings to treat tabs outside of Chrome and block until Chrome
+     * is open on the original tab.
+     */
+    public abstract void unmergeTabs();
+}
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
new file mode 100644
index 0000000..a532263
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractFacebookHelper.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;
+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
new file mode 100644
index 0000000..84c31d9
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractFlightDemoHelper.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;
+
+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/AbstractGmailHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractGmailHelper.java
new file mode 100644
index 0000000..ce36818
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractGmailHelper.java
@@ -0,0 +1,196 @@
+/*
+ * 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 AbstractGmailHelper extends AbstractStandardAppHelper {
+
+    public AbstractGmailHelper(Instrumentation instr) {
+        super(instr);
+    }
+
+    /**
+     * Setup expectations: Gmail is open and the navigation bar is visible.
+     *
+     * This method will navigate to the Inbox or Primary, depending on the name.
+     */
+    public abstract void goToInbox();
+
+    /**
+     * Alias method for AbstractGmailHelper#goToInbox
+     */
+    public void goToPrimary() {
+        goToInbox();
+    }
+
+    /**
+     * Setup expectations: Gmail is open on the Inbox or Primary page.
+     *
+     * This method will open a new e-mail to compose and block until complete.
+     */
+    public abstract void goToComposeEmail();
+
+    /**
+     * Checks if the current view is the compose email view.
+     *
+     * @return true if the current view is the compose email view, false otherwise.
+     */
+    public abstract boolean isInComposeEmail();
+
+    /**
+     * Checks if the app is open on the Inbox or Primary page.
+     *
+     * @return true if the current view is the Inbox or Primary page, false otherwise.
+     */
+    public abstract boolean isInPrimaryOrInbox();
+
+    /**
+     * Setup expectations: Gmail is open and on the Inbox or Primary page.
+     *
+     * This method will open the (index)'th visible e-mail in the list and block until the e-mail is
+     * visible in the foreground. The top-most visible e-mail will always be labeled 0. To get the
+     * number of visible e-mails, consult the getVisibleEmailCount() function.
+     */
+    public abstract void openEmailByIndex(int index);
+
+    /**
+     * Setup expectations: Gmail is open and on the Inbox or Primary page.
+     *
+     * This method will return the number of visible e-mails for use with the #openEmailByIndex
+     * method.
+     */
+    public abstract int getVisibleEmailCount();
+
+    /**
+     * Setup expectations: Gmail is open and an e-mail is open in the foreground.
+     *
+     * This method will press reply, send a reply e-mail with the given parameters, and block until
+     * the original message is in the foreground again.
+     */
+    public abstract void sendReplyEmail(String address, String body);
+
+    /**
+     * Setup expectations: Gmail is open and composing an e-mail.
+     *
+     * This method will set the e-mail's To address and block until complete.
+     */
+    public abstract void setEmailToAddress(String address);
+
+    /**
+     * Setup expectations: Gmail is open and composing an e-mail.
+     *
+     * This method will set the e-mail's subject and block until complete.
+     */
+    public abstract void setEmailSubject(String subject);
+
+    /**
+     * Setup expectations: Gmail is open and composing an e-mail.
+     *
+     * This method will set the e-mail's Body and block until complete. Focus will remain on the
+     * e-mail body after completion.
+     */
+    public abstract void setEmailBody(String body);
+
+    /**
+     * Setup expectations: Gmail is open and composing an e-mail.
+     *
+     * This method will press send and block until the device is idle on the original e-mail.
+     */
+    public abstract void clickSendButton();
+
+    /**
+     * Setup expectations: Gmail is open and composing an e-mail.
+     *
+     * This method will get the e-mail's composition's body and block until complete.
+     *
+     * @return {String} the text contained in the email composition's body.
+     */
+    public abstract String getComposeEmailBody();
+
+    /**
+     * Setup expectations: Gmail is open and the navigation drawer is visible.
+     *
+     * This method will open the navigation drawer and block until complete.
+     */
+    public abstract void openNavigationDrawer();
+
+    /**
+     * Setup expectations: Gmail is open and the navigation drawer is open.
+     *
+     * This method will close the navigation drawer and returns true otherwise false
+     */
+    public abstract boolean closeNavigationDrawer();
+
+    /**
+     * Setup expectations: Gmail is open and the navigation drawer is open.
+     *
+     * This method will scroll the navigation drawer and block until idle. Only accepts UP and DOWN.
+     */
+    public abstract void scrollNavigationDrawer(Direction dir);
+
+    /**
+     * Setup expectations: Gmail is open and a mailbox is open.
+     *
+     * This method will scroll the mailbox view.
+     *
+     * @param direction     The direction to scroll, only accepts UP and DOWN.
+     * @param amount        The amount to scroll
+     * @param scrollToEnd   Whether or not to scroll to the end
+     */
+    public abstract void scrollMailbox(Direction direction, float amount, boolean scrollToEnd);
+
+    /**
+     * Setup expectations: Gmail is open and an email is open.
+     *
+     * This method will scroll the current email.
+     *
+     * @param direction     The direction to scroll, only accepts UP and DOWN.
+     * @param amount        The amount to scroll
+     * @param scrollToEnd   Whether or not to scroll to the end
+     */
+    public abstract void scrollEmail(Direction direction, float amount, boolean scrollToEnd);
+
+    /**
+     * Setup expectations: Gmail is open and the navigation drawer is open.
+     *
+     * This method will open the mailbox with the given name and block until emails in
+     * that mailbox have loaded.
+     *
+     * @param mailboxName The case insensitive name of the mailbox to open
+     */
+    public abstract void openMailbox(String mailboxName);
+
+    /**
+     * Setup expectations: Gmail is open and an email is open.
+     *
+     * This method will return to the mailbox the current email was opened from.
+     */
+    public abstract void returnToMailbox();
+
+    /**
+     * Setup expectations: Gmail is open and an email is open.
+     *
+     * This method starts downloading the attachment at the specified index in the current email.
+     * The download happens in the background. This method returns immediately after starting
+     * the download and does not wait for the download to complete.
+     *
+     * @param index The index of the attachment to download
+     */
+    public abstract void downloadAttachment(int index);
+}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractGoogleCameraHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractGoogleCameraHelper.java
new file mode 100644
index 0000000..dd7efe0
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractGoogleCameraHelper.java
@@ -0,0 +1,139 @@
+/*
+ * 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 AbstractGoogleCameraHelper extends AbstractStandardAppHelper {
+
+    public AbstractGoogleCameraHelper(Instrumentation instr) {
+        super(instr);
+    }
+
+    /**
+     * Setup expectations: GoogleCamera is open and idle in video mode.
+     *
+     * This method will change to camera mode and block until the transition is complete.
+     */
+    public abstract void goToCameraMode();
+
+    /**
+     * Setup expectations: GoogleCamera is open and idle in camera mode.
+     *
+     * This method will change to video mode and block until the transition is complete.
+     */
+    public abstract void goToVideoMode();
+
+    /**
+     * Setup expectations: GoogleCamera is open and idle in either camera/video mode.
+     *
+     * This method will change to back camera and block until the transition is complete.
+     */
+    public abstract void goToBackCamera();
+
+    /**
+     * Setup expectations: GoogleCamera is open and idle in either camera/video mode.
+     *
+     * This method will change to front camera and block until the transition is complete.
+     */
+    public abstract void goToFrontCamera();
+
+    /**
+     * Setup expectation: in Camera mode with the capture button present.
+     *
+     * This method will capture a photo and block until the transaction is complete.
+     */
+    public abstract void capturePhoto();
+
+    /**
+     * Setup expectation: in Video mode with the capture button present.
+     *
+     * This method will capture a video of length timeInMs and block until the transaction is
+     * complete.
+     * @param time duration of video in milliseconds
+     */
+    public abstract void captureVideo(long time);
+
+    /**
+     * Setup expectation:
+     *   1. in Video mode with the capture button present.
+     *   2. videoTime > snapshotStartTime
+     *
+     * This method will capture a video of length videoTime, and take a picture at snapshotStartTime.
+     * It will block until the the video is captured and the device is again idle in video mode.
+     * @param time duration of video in milliseconds
+     */
+    public abstract void snapshotVideo(long videoTime, long snapshotStartTime);
+
+    /**
+     * Setup expectation: GoogleCamera is open and idle in camera mode.
+     *
+     * This method will set HDR to on(1), auto(-1), or off(0).
+     * @param mode the integer value of the mode denoted above.
+     */
+    public abstract void setHdrMode(int mode);
+
+    /**
+     * Setup expectation: GoogleCamera is open and idle in video mode.
+     *
+     * This method will set 4K mode to on(1), or off(0).
+     * @param mode the integer value of the mode denoted above.
+     */
+    public abstract void set4KMode(int mode);
+
+    /**
+     * Setup expectation: GoogleCamera is open and idle in video mode.
+     *
+     * This method will set HFR mode to 240 fps (2), 120 fps (1), or off(0).
+     * @param mode the integer value of the mode denoted above.
+     */
+    public abstract void setHFRMode(int mode);
+
+    /**
+     * Setup expectation: in Camera mode with the capture button present.
+     *
+     * This method will block until the capture button is enabled for pressing.
+     */
+    public abstract void waitForCameraShutterEnabled();
+
+    /**
+     * Setup expectation: in Video mode with the capture button present.
+     *
+     * This method will block until the capture button is enabled for pressing.
+     */
+    public abstract void waitForVideoShutterEnabled();
+
+    /**
+     * Temporary function.
+     */
+    public abstract String openWithShutterTimeString();
+
+    /**
+     * Setup expectations: in Camera mode or in Video mode
+     */
+    public abstract void goToAlbum();
+
+    /**
+     * Setup expectations:
+     *   1. in album view
+     *   2. scroll direction is either LEFT or RIGHT
+     *
+     * @param direction scroll direction, either LEFT or RIGHT
+     */
+    public abstract void scrollAlbum(Direction direction);
+}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractGoogleDocsHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractGoogleDocsHelper.java
new file mode 100644
index 0000000..b041489
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractGoogleDocsHelper.java
@@ -0,0 +1,51 @@
+/*
+ * 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 AbstractGoogleDocsHelper extends AbstractStandardAppHelper {
+
+    public AbstractGoogleDocsHelper(Instrumentation instr) {
+        super(instr);
+    }
+
+    /**
+     * Setup expectation: Google Docs is open and the Recent Docs Tab can be reached by
+     * pressing back button multiple times, i.e. the test procedure has been on the Recent
+     * Docs tab.
+     *
+     * Returns to the Recent Docs Tab.
+     */
+    public abstract void goToRecentDocsTab();
+
+    /**
+     * Setup expectation: Google Docs is on the Recent Docs tab.
+     *
+     * Opens the document.
+     *
+     * @param title The title (case sensitive) of the document as is displayed in the app.
+     */
+    public abstract void openDoc(String title);
+
+    /**
+     * Setup expectation: Google Docs is on a document page.
+     *
+     * Scrolls down the document.
+     */
+    public abstract void scrollDownDocument();
+}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractGoogleKeyboardHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractGoogleKeyboardHelper.java
new file mode 100644
index 0000000..bf072f6
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractGoogleKeyboardHelper.java
@@ -0,0 +1,41 @@
+/*
+ * 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 AbstractGoogleKeyboardHelper extends AbstractStandardAppHelper {
+
+    public AbstractGoogleKeyboardHelper(Instrumentation instr) {
+        super(instr);
+    }
+
+    /*
+     * Setup expectations: Recently performed action that will open Google Keyboard
+     *
+     * @param timeout wait timeout in milliseconds
+     */
+    public abstract boolean waitForKeyboard(long timeout);
+
+    /*
+     * Setup expectations: Google Keyboard is open and visible
+     *
+     * @param text text to type
+     * @param delayBetweenKeyPresses delay between key presses in milliseconds
+     */
+    public abstract void typeText(String text, long delayBetweenKeyPresses);
+}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractGoogleMessengerHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractGoogleMessengerHelper.java
new file mode 100644
index 0000000..f051c39
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractGoogleMessengerHelper.java
@@ -0,0 +1,79 @@
+/*
+ * 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 AbstractGoogleMessengerHelper extends AbstractStandardAppHelper {
+
+    public AbstractGoogleMessengerHelper(Instrumentation instr) {
+        super(instr);
+    }
+
+    /*
+     * Setup expectations: Google Messenger app is open.
+     *
+     * This method brings the Messenger app to the home page.
+     */
+    public abstract void goToHomePage();
+
+    /*
+     * Setup expectation: Google Messenger app is on the home page.
+     *
+     * This method brings up the new conversation page.
+     */
+    public abstract void goToNewConversationPage();
+
+    /*
+     * Setup expectations: Google Messenger app is on the new conversation page.
+     *
+     * This method moves the Google Messenger app to messages page with the specified contacts.
+     */
+    public abstract void goToMessagesPage();
+
+    /**
+     * Setup expectations: Google Messenger app is on the messages page.
+     *
+     * This method scrolls through the messages on the messages page.
+     *
+     * @param direction Direction to scroll, must be UP or DOWN.
+     */
+    public abstract void scrollMessages(Direction direction);
+
+    /**
+     * Setup expectations: Google Messenger app is on the messages page.
+     *
+     * This method clicks the "send message" textbox on the messages page.
+     */
+    public abstract void clickComposeMessageText();
+
+    /**
+     * Setup expectations:
+     *   1. Google Messenger app is on the messages page
+     *   2. New message textbox is not empty.
+     */
+    public abstract void clickSendMessageButton();
+
+    /**
+     * Setup expectations: Google Messenger app is on the messages page.
+     *
+     * This method clicks the "attach media" button and attaches the media file with the given
+     * index in the device media gallery view.
+     */
+    public abstract void attachMediaFromDevice(int index);
+}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractLeanbackAppHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractLeanbackAppHelper.java
new file mode 100644
index 0000000..e81bcfe
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractLeanbackAppHelper.java
@@ -0,0 +1,192 @@
+/*
+ * 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.platform.test.helpers.exceptions.UnknownUiException;
+import android.support.test.launcherhelper.ILeanbackLauncherStrategy;
+import android.support.test.launcherhelper.LauncherStrategyFactory;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.util.Log;
+
+/**
+ *  This app helper handles the following important widgets for TV apps:
+ *  BrowseFragment, DetailsFragment, SearchFragment and PlaybackOverlayFragment
+ */
+public abstract class AbstractLeanbackAppHelper extends AbstractStandardAppHelper {
+
+    private static final String TAG = AbstractLeanbackAppHelper.class.getSimpleName();
+    private static final long OPEN_SECTION_WAIT_TIME_MS = 5000;
+    private static final long OPEN_SIDE_PANEL_WAIT_TIME_MS = 5000;
+    private static final int OPEN_SIDE_PANEL_MAX_ATTEMPTS = 5;
+    private static final long MAIN_ACTIVITY_WAIT_TIME_MS = 250;
+
+    protected DPadHelper mDPadHelper;
+    public ILeanbackLauncherStrategy mLauncherStrategy;
+
+
+    public AbstractLeanbackAppHelper(Instrumentation instr) {
+        super(instr);
+        mDPadHelper = DPadHelper.getInstance(instr);
+        mLauncherStrategy = LauncherStrategyFactory.getInstance(
+                mDevice).getLeanbackLauncherStrategy();
+    }
+
+    protected abstract BySelector getAppSelector();
+
+    protected abstract BySelector getSidePanelSelector();
+
+    protected abstract BySelector getSidePanelResultSelector(String sectionName);
+
+    /**
+     * Selector to identify main activity for getMainActivitySelector().
+     * Not every application has its main activity, so the override is optional.
+     */
+    protected BySelector getMainActivitySelector() {
+        return null;
+    }
+
+    /**
+     * Setup expectation: Side panel is selected on browse fragment
+     *
+     * Best effort attempt to go to the side panel, and open the selected section.
+     */
+    public void openSection(String sectionName) {
+        openSidePanel();
+        // Section header is focused; it should not be after pressing the DPad
+        selectSection(sectionName);
+        mDevice.pressDPadCenter();
+
+        // Test for focus change and selection result
+        BySelector sectionResult = getSidePanelResultSelector(sectionName);
+        if (!mDevice.wait(Until.hasObject(sectionResult), OPEN_SECTION_WAIT_TIME_MS)) {
+            throw new UnknownUiException(
+                    String.format("Failed to find result opening section %s", sectionName));
+        }
+        Log.v(TAG, "Successfully opened section");
+    }
+
+    /**
+     * Setup expectation: On navigation screen on browse fragment
+     *
+     * Best effort attempt to open the side panel.
+     * @param onMainActivity True if it opens the side panel on app's main activity.
+     */
+    public void openSidePanel(boolean onMainActivity) {
+        if (onMainActivity) {
+            returnToMainActivity();
+        }
+        int attempts = 0;
+        while (!isSidePanelSelected(OPEN_SIDE_PANEL_WAIT_TIME_MS)
+                && attempts++ < OPEN_SIDE_PANEL_MAX_ATTEMPTS) {
+            mDevice.pressDPadLeft();
+        }
+        if (attempts == OPEN_SIDE_PANEL_MAX_ATTEMPTS) {
+            throw new UnknownUiException("Failed to open side panel");
+        }
+    }
+
+    public void openSidePanel() {
+        openSidePanel(false);
+    }
+
+    /**
+     * Select target item through the container in the given direction.
+     * @param container
+     * @param target
+     * @param direction
+     * @return the focused object
+     */
+    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 focus.");
+        }
+        while (!focus.hasObject(target)) {
+            UiObject2 prev = focus;
+            mDPadHelper.pressDPad(direction);
+            focus = container.findObject(By.focused(true));
+            if (focus == null) {
+                mDPadHelper.pressDPad(Direction.reverse(direction));
+                focus = container.findObject(By.focused(true));
+            }
+            if (focus.equals(prev)) {
+                // It reached at the end, but no target is found.
+                return null;
+            }
+        }
+        return focus;
+    }
+
+    /**
+     * Attempts to return to main activity with getMainActivitySelector()
+     * by pressing the back button repeatedly and sleeping briefly to allow for UI slowness.
+     */
+    public void returnToMainActivity() {
+        int maxBackAttempts = 10;
+        BySelector selector = getMainActivitySelector();
+        if (selector == null) {
+            throw new IllegalStateException("getMainActivitySelector() should be overridden.");
+        }
+        while (!mDevice.wait(Until.hasObject(selector), MAIN_ACTIVITY_WAIT_TIME_MS)
+                && maxBackAttempts-- > 0) {
+            mDevice.pressBack();
+        }
+    }
+
+    @Override
+    public void dismissInitialDialogs() {
+        return;
+    }
+
+    protected boolean isSidePanelSelected(long timeout) {
+        UiObject2 sidePanel = mDevice.wait(Until.findObject(getSidePanelSelector()), timeout);
+        if (sidePanel == null) {
+            return false;
+        }
+        return sidePanel.hasObject(By.focused(true).minDepth(1));
+    }
+
+    protected UiObject2 selectSection(String sectionName) {
+        UiObject2 container = mDevice.wait(
+                Until.findObject(getSidePanelSelector()), OPEN_SIDE_PANEL_WAIT_TIME_MS);
+        BySelector section = By.clazz(".TextView").text(sectionName);
+
+        // Wait until the section text appears at runtime. This needs to be long enough to run under
+        // low bandwidth environments in the test lab.
+        mDevice.wait(Until.findObject(section), 60 * 1000);
+
+        // Search up, then down
+        UiObject2 focused = select(container, section, Direction.UP);
+        if (focused != null) {
+            return focused;
+        }
+        focused = select(container, section, Direction.DOWN);
+        if (focused != null) {
+            return focused;
+        }
+        throw new UnknownUiException("Failed to select section");
+    }
+
+}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractMapsHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractMapsHelper.java
new file mode 100644
index 0000000..6c9f72f
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractMapsHelper.java
@@ -0,0 +1,55 @@
+/*
+ * 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 AbstractMapsHelper extends AbstractStandardAppHelper {
+
+    public AbstractMapsHelper(Instrumentation instr) {
+        super(instr);
+    }
+
+    /**
+     * 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/AbstractPhotosHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractPhotosHelper.java
new file mode 100644
index 0000000..b94ea5e
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractPhotosHelper.java
@@ -0,0 +1,128 @@
+/*
+ * 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 AbstractPhotosHelper extends AbstractStandardAppHelper {
+
+    public AbstractPhotosHelper(Instrumentation instr) {
+        super(instr);
+    }
+
+    /**
+     * Setup expectations: Photos is open and on the main or a device folder's screen.
+     *
+     * This method will check if there are videos that can be opened on current screen
+     * by looking for any view objects with content-desc that starts with "Video"
+     *
+     * @return true if video clips are found, false otherwise
+     */
+    public abstract boolean searchForVideoClip();
+
+    /**
+     * Setup expectations: Photos is open and on the main or a device folder's screen.
+     *
+     * This method will select the first clip to open and play. This will block until the clip
+     * begins to plays.
+     */
+    public abstract void openFirstClip();
+
+    /**
+     * Setup expectations: Photos is open and a clip is currently playing.
+     *
+     * This method will pause the current clip and block until paused.
+     */
+    public abstract void pauseClip();
+
+    /**
+     * Setup expectations: Photos is open and a clip is currently paused in the foreground.
+     *
+     * This method will play the current clip and block until it is playing.
+     */
+    public abstract void playClip();
+
+    /**
+     * Setup expectations: Photos is open.
+     *
+     * This method will go to the main screen.
+     */
+    public abstract void goToMainScreen();
+
+    /**
+     * Setup expectations: Photos is open.
+     *
+     * This method will go to device folder screen.
+     */
+    public abstract void goToDeviceFolderScreen();
+
+    /**
+     * Setup expectations:
+     *   1. Photos is open
+     *   2. on device folder screen
+     *   3. the first device folder is shown on the screen
+     *
+     * This method will search for user-specified device folder in device folders.
+     * If the device folder is found, the function will return with the device
+     * folder on current screen.
+     *
+     * @param folderName  User-specified device folder name
+     * @return true if device folder is found, false otherwise
+     */
+    public abstract boolean searchForDeviceFolder(String folderName);
+
+    /**
+     * Setup expectations:
+     *   1. Photos is open
+     *   2. on device folder screen
+     *   3. user-specified device folder is currently on screen
+     *
+     * This method will open the user-specified device folder.
+     *
+     * @param folderName  User-specified device folder name
+     */
+    public abstract void openDeviceFolder(String folderName);
+
+    /**
+     * Setup expectations: Photos is open and on the main or a device folder's screen.
+     *
+     * This method will check if there are pictures that can be opened on current screen
+     * by looking for any view objects with content-desc that starts with "Photo"
+     *
+     * @return true if pictures are found
+     */
+    public abstract boolean searchForPicture();
+
+    /**
+     * Setup expectations: Photos is open and on the main or a device folder's screen.
+     *
+     * This method will open the picture at the specified index.
+     *
+     * @param index The index of the picture to open
+     */
+    public abstract void openPicture(int index);
+
+    /**
+     * Setup expectations: Photos is open and a picture album is open.
+     *
+     * This method will scroll the picture album in the specified direction.
+     *
+     * @param direction The direction to scroll, must be LEFT or RIGHT.
+     */
+    public abstract void scrollAlbum(Direction direction);
+}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractPlayBooksHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractPlayBooksHelper.java
new file mode 100644
index 0000000..d0b0c28
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractPlayBooksHelper.java
@@ -0,0 +1,75 @@
+/*
+ * 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 AbstractPlayBooksHelper extends AbstractStandardAppHelper {
+
+    public AbstractPlayBooksHelper(Instrumentation instr) {
+        super(instr);
+    }
+
+    /**
+     * Setup expectations: PlayBooks is open on any screen.
+     *
+     * Navigates to "My Library" and selects the "ALL BOOKS" tab.
+     */
+    public abstract void goToAllBooksTab();
+
+    /**
+     * Setup expectations: PlayBooks is open on "My Library - ALL BOOKS" screen.
+     *
+     * Selects the first Book and start reading.
+     */
+    public abstract void openBook();
+
+    /**
+     * Setup expectations: PlayBooks is on a page of a book.
+     *
+     * Exits reading mode.
+     */
+    public abstract void exitReadingMode();
+
+    /**
+     * Setup expectations: PlayBooks is on a full-screen page of a book.
+     *
+     * Goes to the next page by clicking the right side of the page.
+     */
+    public abstract void goToNextPage();
+
+    /**
+     * Setup expectations: PlayBooks is on a full-screen page of a book.
+     *
+     * Goes to the previous page by clicking the left side of the page.
+     */
+    public abstract void goToPreviousPage();
+
+    /**
+     * Setup expectations: PlayBooks is on a full-screen page of a book.
+     *
+     * Goes to the next page by scrolling leftwards.
+     */
+    public abstract void scrollToNextPage();
+
+    /**
+     * Setup expectations: PlayBooks is on a full-screen page of a book.
+     *
+     * Goes to the previous page by scrolling rightwards.
+     */
+    public abstract void scrollToPreviousPage();
+}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractPlayMoviesHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractPlayMoviesHelper.java
new file mode 100644
index 0000000..56ce8e6
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractPlayMoviesHelper.java
@@ -0,0 +1,41 @@
+/*
+ * 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 AbstractPlayMoviesHelper extends AbstractStandardAppHelper {
+
+    public AbstractPlayMoviesHelper(Instrumentation instr) {
+        super(instr);
+    }
+
+    /**
+     * Setup expectations: PlayMovies is open on any screen with access to the navigation bar.
+     *
+     * This method will navigate to "My Library" and select the "My Movies" tab. This will block
+     * until the method is complete.
+     */
+    public abstract void openMoviesTab();
+
+    /**
+     * Setup expectations: PlayMovies is open on any screen.
+     *
+     * PlayMovies will select the movie card and subsequently press the play button.
+     */
+    public abstract void playMovie(String name);
+}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractPlayMusicHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractPlayMusicHelper.java
new file mode 100644
index 0000000..352e8ea
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractPlayMusicHelper.java
@@ -0,0 +1,79 @@
+/*
+ * 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 AbstractPlayMusicHelper extends AbstractStandardAppHelper {
+
+    public AbstractPlayMusicHelper(Instrumentation instr) {
+        super(instr);
+    }
+
+    /**
+     * Setup expectations: PlayMusic is open and the navigation bar is visible.
+     *
+     * This method will open the navigation bar, press "My Library," and navigate to the songs tab.
+     * This method blocks until the process is complete.
+     */
+    public abstract void goToTab(String tabTitle);
+
+    /**
+     * Setup expectations: PlayMusic is open and the navigation bar is visible.
+     *
+     * This method will navigate to the Albums tab, select the album, and then select the song. The
+     * method will block until the song is playing.
+     */
+    public abstract void selectSong(String album, String song);
+
+    /**
+     * Setup expectations: PlayMusic is open with a song playing.
+     *
+     * This method will pause the song and block until the song is paused.
+     */
+    public abstract void pauseSong();
+
+    /**
+     * Setup expectations: PlayMusic is open with a song paused.
+     *
+     * This method will play the song and block until the song is playing.
+     */
+    public abstract void playSong();
+
+    /**
+     * Setup expectations: PlayMusic is open with a song playing the controls minimized.
+     *
+     * This method will press the header and block until the song is expanded.
+     */
+    public abstract void expandMediaControls();
+
+    /**
+     * Setup expectations: PlayMusic is open and on the Songs library tab
+     *
+     * This method will press the "Shuffle All" button and block until the song is playing.
+     */
+    public abstract void pressShuffleAll();
+
+    /**
+     * Setup expectations: PlayMusic is open with a song open and expanded.
+     *
+     * This method will press the repeat button and cycle to the next state. Unfortunately, the
+     * limitations of the Accessibility for Play Music means that we cannot tell what state it
+     * currently is in.
+     */
+    public abstract void pressRepeat();
+}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractPlayStoreHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractPlayStoreHelper.java
new file mode 100644
index 0000000..723565a
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractPlayStoreHelper.java
@@ -0,0 +1,42 @@
+/*
+ * 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 AbstractPlayStoreHelper extends AbstractStandardAppHelper {
+
+    public AbstractPlayStoreHelper(Instrumentation instr) {
+        super(instr);
+    }
+
+    /**
+     * Setup expectations: The search bar is visible.
+     *
+     * Selects the search bar, enters a query, and displays the results. Blocks until the results
+     * are selectable.
+     */
+    public abstract void doSearch(String query);
+
+    /**
+     * Setup expectations: There are visible search results.
+     *
+     * Opens the necessary categories and enters the first search result. Blocks until the process
+     * is complete.
+     */
+    public abstract void selectFirstResult();
+}
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
new file mode 100644
index 0000000..7a1a85b
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractRecentsHelper.java
@@ -0,0 +1,35 @@
+/*
+ * 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
new file mode 100644
index 0000000..7cf83a0
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractRedditHelper.java
@@ -0,0 +1,63 @@
+/*
+ * 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/AbstractSettingsHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractSettingsHelper.java
new file mode 100644
index 0000000..eab8188
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractSettingsHelper.java
@@ -0,0 +1,44 @@
+/*
+ * 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 AbstractSettingsHelper extends AbstractStandardAppHelper {
+
+    public AbstractSettingsHelper(Instrumentation instr) {
+        super(instr);
+    }
+
+    /**
+     * Setup expectation: Settings is open.
+     *
+     * This method will fling through the settings list numberOfFlings
+     * number of times or until the bottom of Settings is reached,
+     * whichever happens first.
+     * @param numberOfFlings number of flings needed
+     */
+    public abstract void scrollThroughSettings(int numberOfFlings) throws Exception;
+
+    /**
+     * Setup expectation: Settings is open.
+     *
+     * This method will fling through the settings list until it
+     * reaches the top of the list.
+     */
+    public abstract void flingSettingsToStart() throws Exception;
+}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractStandardAppHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractStandardAppHelper.java
new file mode 100644
index 0000000..93fd1fe
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractStandardAppHelper.java
@@ -0,0 +1,91 @@
+/*
+ * 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.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.support.test.launcherhelper.ILauncherStrategy;
+import android.support.test.launcherhelper.LauncherStrategyFactory;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+
+public abstract class AbstractStandardAppHelper implements IStandardAppHelper {
+    public UiDevice mDevice;
+    public Instrumentation mInstrumentation;
+    public ILauncherStrategy mLauncherStrategy;
+
+    public AbstractStandardAppHelper(Instrumentation instr) {
+        mInstrumentation = instr;
+        mDevice = UiDevice.getInstance(instr);
+        mLauncherStrategy = LauncherStrategyFactory.getInstance(mDevice).getLauncherStrategy();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void open() {
+        String pkg = getPackage();
+        String id = getLauncherName();
+        if (!mDevice.hasObject(By.pkg(pkg).depth(0))) {
+            mLauncherStrategy.launch(id, pkg);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void exit() {
+        int maxBacks = 4;
+        while (!mDevice.hasObject(mLauncherStrategy.getWorkspaceSelector()) && maxBacks > 0) {
+            mDevice.pressBack();
+            mDevice.waitForIdle();
+            maxBacks--;
+        }
+
+        if (maxBacks == 0) {
+            mDevice.pressHome();
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getVersion() throws NameNotFoundException {
+        String pkg = getPackage();
+
+        if (null == pkg || pkg.isEmpty()) {
+            throw new RuntimeException("Cannot find version of empty package");
+        }
+        PackageManager pm = mInstrumentation.getContext().getPackageManager();
+        PackageInfo pInfo = pm.getPackageInfo(pkg, 0);
+        String version = pInfo.versionName;
+        if (null == version || version.isEmpty()) {
+            throw new RuntimeException(String.format("Version isn't found for package, %s", pkg));
+        }
+
+        return version;
+    }
+
+    protected int getOrientation() {
+        return mInstrumentation.getContext().getResources().getConfiguration().orientation;
+    }
+}
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
new file mode 100644
index 0000000..1e5ec95
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractTuneInHelper.java
@@ -0,0 +1,64 @@
+/*
+ * 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/base-app-helpers/src/android/platform/test/helpers/AbstractYouTubeHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractYouTubeHelper.java
new file mode 100644
index 0000000..aa0c641
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractYouTubeHelper.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 android.platform.test.helpers;
+
+import android.app.Instrumentation;
+
+public abstract class AbstractYouTubeHelper extends AbstractStandardAppHelper {
+
+    public enum VideoQuality {
+        QUALITY_AUTO ("Auto"),
+        QUALITY_144p ("144p"),
+        QUALITY_240p ("240p"),
+        QUALITY_360p ("360p"),
+        QUALITY_480p ("480p"),
+        QUALITY_720p ("720p"),
+        QUALITY_1080p("1080p");
+
+        private final String text;
+
+        VideoQuality(String text) {
+            this.text = text;
+        }
+
+        public String getText() {
+            return text;
+        }
+    };
+
+    public AbstractYouTubeHelper(Instrumentation instr) {
+        super(instr);
+    }
+
+    /**
+     * Setup expectations: YouTube app is open.
+     *
+     * This method keeps pressing the back button until YouTube is on the home page.
+     */
+    public abstract void goToHomePage();
+
+    /**
+     * Setup expectations: YouTube is on the home page.
+     *
+     * This method scrolls to the top of the home page and clicks the search button.
+     */
+    public abstract void goToSearchPage();
+
+    /**
+     * Setup expectations: YouTube is on the non-fullscreen video player page.
+     *
+     * This method changes the video player to fullscreen mode. Has no effect if the video player
+     * is already in fullscreen mode.
+     */
+    public abstract void goToFullscreenMode();
+
+    /**
+     * Setup expectations: YouTube is on the home page.
+     *
+     * This method selects a video on the home page and blocks until the video is playing.
+     */
+    public abstract void playHomePageVideo();
+
+    /**
+     * Setup expectations: YouTube is on the search results page.
+     *
+     * This method selects a search result video and blocks until the video is playing.
+     */
+    public abstract void playSearchResultPageVideo();
+
+    /**
+     * Setup expectations: Recently opened a video in the YouTube app.
+     *
+     * This method blocks until the video has loaded.
+     *
+     * @param timeout wait timeout in milliseconds
+     * @return true if video loaded within the timeout, false otherwise
+     */
+    public abstract boolean waitForVideoToLoad(long timeout);
+
+    /**
+     * Setup expectations: Recently initiated a search query in the YouTube app.
+     *
+     * This method blocks until search results appear.
+     *
+     * @param timeout wait timeout in milliseconds
+     * @return true if search results appeared within timeout, false otherwise
+     */
+    public abstract boolean waitForSearchResults(long timeout);
+
+    /**
+     * Setup expectations: YouTube is on the video player page.
+     *
+     * This method changes the video quality of the current video.
+     *
+     * @param quality   the desired video quality
+     * @see AbstractYouTubeHelper.VideoQuality
+     */
+    public abstract void setVideoQuality(VideoQuality quality);
+
+    /**
+     * Setup expectations: YouTube is on the video player page.
+     *
+     * This method resumes the video if it is paused.
+     */
+    public abstract void resumeVideo();
+}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/DPadHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/DPadHelper.java
new file mode 100644
index 0000000..51dd12b
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/DPadHelper.java
@@ -0,0 +1,79 @@
+/*
+ * 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.os.SystemClock;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiDevice;
+
+public class DPadHelper {
+
+    private static final String TAG = DPadHelper.class.getSimpleName();
+    private static final long DPAD_DEFAULT_WAIT_TIME_MS = 1000; // 1 sec
+    private static DPadHelper mInstance;
+    private UiDevice mDevice;
+
+
+    private DPadHelper(Instrumentation instrumentation) {
+        mDevice = UiDevice.getInstance(instrumentation);
+    }
+
+    public static DPadHelper getInstance(Instrumentation instrumentation) {
+        if (mInstance == null) {
+            mInstance = new DPadHelper(instrumentation);
+        }
+        return mInstance;
+    }
+
+    public void pressDPad(Direction direction) {
+        pressDPad(direction, 1, DPAD_DEFAULT_WAIT_TIME_MS);
+    }
+
+    public void pressDPad(Direction direction, long repeat) {
+        pressDPad(direction, repeat, DPAD_DEFAULT_WAIT_TIME_MS);
+    }
+
+    /**
+     * Presses DPad button of the same direction for the count times.
+     * It sleeps between each press for DPAD_DEFAULT_WAIT_TIME_MS.
+     *
+     * @param direction the direction of the button to press.
+     * @param repeat the number of times to press the button.
+     * @param timeout the timeout for the wait.
+     */
+    public void pressDPad(Direction direction, long repeat, long timeout) {
+        int iteration = 0;
+        while (iteration++ < repeat) {
+            switch (direction) {
+                case LEFT:
+                    mDevice.pressDPadLeft();
+                    break;
+                case RIGHT:
+                    mDevice.pressDPadRight();
+                    break;
+                case UP:
+                    mDevice.pressDPadUp();
+                    break;
+                case DOWN:
+                    mDevice.pressDPadDown();
+                    break;
+            }
+            SystemClock.sleep(timeout);
+        }
+    }
+}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/IStandardAppHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/IStandardAppHelper.java
new file mode 100644
index 0000000..3c5cde1
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/IStandardAppHelper.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.platform.test.helpers;
+
+import android.content.pm.PackageManager.NameNotFoundException;
+
+public interface IStandardAppHelper {
+
+    /**
+     * Setup expectation: On the launcher home screen.
+     *
+     * Launches the desired application.
+     */
+    abstract void open();
+
+    /**
+     * Setup expectation: None
+     *
+     * Presses back until the launcher package is visible, i.e. the home screen. This can be
+     * overriden for custom functionality, however consider and document the exit state if doing so.
+     */
+    abstract void exit();
+
+    /**
+     * Setup expectations: This application is on the initial launch screen.
+     *
+     * This method will dismiss all visible relevant dialogs and block until this process is
+     * complete.
+     */
+    abstract void dismissInitialDialogs();
+
+    /**
+     * Setup expectations: None
+     *
+     * @return the package name for this helper's application.
+     */
+    abstract String getPackage();
+
+    /**
+     * Setup expectations: None.
+     *
+     * @return the name for this application in the launcher.
+     */
+    abstract String getLauncherName();
+
+    /**
+     * Setup expectations: None
+     *
+     * This method will return the version String from PackageManager.
+     * @param pkgName the application package
+     * @throws NameNotFoundException if the package is not found in PM
+     * @return the version as a String
+     */
+    abstract String getVersion() throws NameNotFoundException;
+}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/exceptions/UiTimeoutException.java b/libraries/base-app-helpers/src/android/platform/test/helpers/exceptions/UiTimeoutException.java
new file mode 100644
index 0000000..3aee637
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/exceptions/UiTimeoutException.java
@@ -0,0 +1,35 @@
+/*
+ * 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.exceptions;
+
+/**
+ * A UiTimeoutException is an exception specific to UI-driven app helpers. This should be thrown
+ * when a specific UI condition is not met due to a timeout that has been exceeded.
+ * <p>
+ * Examples include (but are not limited to): waiting for the shutter button to be enabled in GCA
+ * or long loading times for Gmail. The reason or symptom may be clarified by the included message,
+ * but should not speculate if there is any reasonable doubt.
+ */
+public class UiTimeoutException extends RuntimeException {
+    public UiTimeoutException(String msg) {
+        super(msg);
+    }
+
+    public UiTimeoutException(String msg, Throwable cause) {
+        super(msg, cause);
+    }
+}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/exceptions/UnknownUiException.java b/libraries/base-app-helpers/src/android/platform/test/helpers/exceptions/UnknownUiException.java
new file mode 100644
index 0000000..b06df06
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/exceptions/UnknownUiException.java
@@ -0,0 +1,37 @@
+/*
+ * 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.exceptions;
+
+/**
+ * An UnknownUiException is an exception specific to UI-driven app helpers. This should be thrown
+ * when specific UI conditions, generally post-conditions, are not met for some unknown reason.
+ * <p>
+ * Examples include (but are not limited to): opening an e-mail and not finding any open message,
+ * loading a website and not seeing any content, being on GCA in camera mode without a flash button.
+ * <p>
+ * These exceptions are likely a manifestation of unhandled conditions or UI updates, but cannot
+ * explicitly say so without further diagnosis.
+ */
+public class UnknownUiException extends RuntimeException {
+    public UnknownUiException(String msg) {
+        super(msg);
+    }
+
+    public UnknownUiException(String msg, Throwable cause) {
+        super(msg, cause);
+    }
+}
diff --git a/libraries/chrome-app-helper/Android.mk b/libraries/chrome-app-helper/Android.mk
new file mode 100644
index 0000000..7f0c1d0
--- /dev/null
+++ b/libraries/chrome-app-helper/Android.mk
@@ -0,0 +1,23 @@
+#
+# 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.
+#
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := chrome-app-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator base-app-helpers
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/chrome-app-helper/src/android/platform/test/helpers/ChromeHelperImpl.java b/libraries/chrome-app-helper/src/android/platform/test/helpers/ChromeHelperImpl.java
new file mode 100644
index 0000000..25d8755
--- /dev/null
+++ b/libraries/chrome-app-helper/src/android/platform/test/helpers/ChromeHelperImpl.java
@@ -0,0 +1,307 @@
+/*
+ * 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.os.SystemClock;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.util.Log;
+import android.webkit.WebView;
+import android.widget.ListView;
+
+import java.io.IOException;
+
+public class ChromeHelperImpl extends AbstractChromeHelper {
+    private static final String LOG_TAG = ChromeHelperImpl.class.getSimpleName();
+
+    private static final String UI_MENU_BUTTON_ID = "menu_button";
+    private static final String UI_SEARCH_BOX_ID = "search_box_text";
+    private static final String UI_URL_BAR_ID = "url_bar";
+    private static final String UI_VIEW_HOLDER_ID = "compositor_view_holder";
+    private static final String UI_POSITIVE_BUTTON_ID = "positive_button";
+    private static final String UI_NEGATIVE_BUTTON_ID = "negative_button";
+
+    private static final long APP_INIT_WAIT = 10000;
+    private static final long MAX_DIALOG_TRANSITION = 5000;
+    private static final long PAGE_LOAD_TIMEOUT = 30 * 1000;
+    private static final long ANIMATION_TIMEOUT = 3000;
+
+    private String mPackageName;
+    private String mLauncherName;
+
+    public ChromeHelperImpl(Instrumentation instr) {
+        super(instr);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getPackage() {
+        if (mPackageName == null) {
+            String prop = null;
+            try {
+                mDevice.executeShellCommand("getprop dev.chrome.package");
+            } catch (IOException ioe) {
+                // log but ignore
+                Log.e(LOG_TAG, "IOException while getprop", ioe);
+            }
+            if (prop == null || prop.isEmpty()) {
+                prop = "com.android.chrome";
+            }
+            mPackageName = prop;
+        }
+        return mPackageName;
+    }
+
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getLauncherName() {
+        if (mLauncherName == null) {
+            String prop = null;
+            try {
+                mDevice.executeShellCommand("getprop dev.chrome.name");
+            } catch (IOException ioe) {
+                // log but ignore
+                Log.e(LOG_TAG, "IOException while getprop", ioe);
+            }
+            if (prop == null || prop.isEmpty()) {
+                prop = "Chrome";
+            }
+            mLauncherName = prop;
+        }
+        return mLauncherName;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void dismissInitialDialogs() {
+        // Terms of Service
+        UiObject2 tos = mDevice.wait(Until.findObject(By.res(getPackage(), "terms_accept")),
+                APP_INIT_WAIT);
+        if (tos != null) {
+            tos.click();
+        }
+
+        if (!hasAccountRegistered()) {
+            // Device has no accounts registered that Chrome recognizes
+            // Select negative button to skip setup wizard sign in
+            UiObject2 negative = mDevice.wait(Until.findObject(
+                    By.res(getPackage(), UI_NEGATIVE_BUTTON_ID)), MAX_DIALOG_TRANSITION);
+
+            if (negative != null) {
+                negative.click();
+            }
+        } else {
+            // Device has an account registered that Chrome recognizes
+            // Press positive buttons until through setup wizard
+            for (int i = 0; i < 4; i++) {
+                if (!isInSetupWizard()) {
+                    break;
+                }
+
+                UiObject2 positive = mDevice.wait(Until.findObject(
+                        By.res(getPackage(), UI_POSITIVE_BUTTON_ID)), MAX_DIALOG_TRANSITION);
+                if (positive != null) {
+                    positive.click();
+                }
+            }
+        }
+
+        mDevice.wait(Until.findObject(By.res(getPackage(), UI_SEARCH_BOX_ID)),
+                MAX_DIALOG_TRANSITION);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void openUrl(String url) {
+        UiObject2 urlBar = getUrlBar();
+        if (urlBar == null) {
+            throw new IllegalStateException("Failed to detect a URL bar");
+        }
+
+        mDevice.waitForIdle();
+        urlBar.setText(url);
+        mDevice.pressEnter();
+        waitForPageLoad();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void flingPage(Direction dir) {
+        UiObject2 page = getWebPage();
+        if (page != null) {
+            int minDim = Math.min(
+                    page.getVisibleBounds().width(), page.getVisibleBounds().height());
+            page.setGestureMargin((int)Math.floor(minDim * 0.25));
+            page.fling(dir);
+        } else {
+            Log.e(LOG_TAG, String.format("Failed to fling page %s", dir.toString()));
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void openMenu() {
+        UiObject2 menuButton = null;
+        for (int retries = 2; retries > 0; retries--) {
+            menuButton = mDevice.findObject(By.desc("More options"));
+            if (menuButton == null) {
+                flingPage(Direction.UP);
+            } else {
+                break;
+            }
+        }
+
+        if (menuButton == null) {
+            throw new IllegalStateException("Unable to find menu button.");
+        }
+        menuButton.clickAndWait(Until.newWindow(), 5000);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void mergeTabs() {
+        openSettings();
+        mDevice.findObject(By.text("Merge tabs and apps")).click();
+        if (mDevice.findObject(By.text("On")) != null) {
+            // Merge tabs is already on
+            mDevice.pressBack();
+            mDevice.pressBack();
+        } else {
+            mDevice.findObject(By.res(getPackage(), "switch_widget")).click();
+            mDevice.findObject(By.text("OK")).click();
+        }
+        SystemClock.sleep(5000);
+        waitForPageLoad();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void unmergeTabs() {
+        openSettings();
+        mDevice.findObject(By.text("Merge tabs and apps")).click();
+        if (mDevice.findObject(By.text("Off")) != null) {
+            // Merge tabs is already off
+            mDevice.pressBack();
+            mDevice.pressBack();
+        } else {
+            mDevice.findObject(By.res(getPackage(), "switch_widget")).click();
+            mDevice.findObject(By.text("OK")).click();
+        }
+        SystemClock.sleep(5000);
+        waitForPageLoad();
+    }
+
+    private void openSettings() {
+        openMenu();
+        UiObject2 menu = getMenu();
+        // TODO: Change this to be non-constant
+        menu.setGestureMargin(500);
+        menu.scroll(Direction.DOWN, 1.0f);
+        // Open the Settings menu
+        mDevice.findObject(By.desc("Settings")).clickAndWait(Until.newWindow(), 3000);
+    }
+
+    private UiObject2 getWebPage() {
+        mDevice.waitForIdle();
+
+        UiObject2 webView = mDevice.findObject(By.clazz(WebView.class));
+        if (webView != null) {
+            return webView;
+        }
+
+        UiObject2 viewHolder = mDevice.findObject(
+                By.res(getPackage(), UI_VIEW_HOLDER_ID));
+        return viewHolder;
+    }
+
+    private UiObject2 getUrlBar() {
+        // First time, URL bar is has id SEARCH_BOX_ID
+        UiObject2 urlLoc = mDevice.findObject(By.res(getPackage(), UI_SEARCH_BOX_ID));
+        if (urlLoc != null) {
+            urlLoc.click();
+            // Waits for the animation to complete.
+            mDevice.wait(Until.findObject(By.res(getPackage(), UI_URL_BAR_ID)), ANIMATION_TIMEOUT);
+        }
+
+        // Afterwards, URL bar has id URL_BAR_ID; must re-select
+        for (int retries = 2; retries > 0; retries--) {
+            urlLoc = mDevice.findObject(By.res(getPackage(), UI_URL_BAR_ID));
+            if (urlLoc == null) {
+                flingPage(Direction.UP);
+            } else {
+                break;
+            }
+        }
+
+        if (urlLoc != null) {
+            urlLoc.click();
+        } else {
+            throw new IllegalStateException("Failed to find a URL bar.");
+        }
+
+        return urlLoc;
+    }
+
+    private UiObject2 getMenu() {
+        return mDevice.findObject(By.clazz(ListView.class).pkg(getPackage()));
+    }
+
+    private void waitForPageLoad() {
+        mDevice.waitForIdle();
+        if (mDevice.hasObject(By.desc("Stop page loading"))) {
+            mDevice.wait(Until.gone(By.desc("Stop page loading")), PAGE_LOAD_TIMEOUT);
+        } else if (mDevice.hasObject(By.res(getPackage(), "progress"))) {
+            mDevice.wait(Until.gone(By.res(getPackage(), "progress")), PAGE_LOAD_TIMEOUT);
+        }
+    }
+
+    private boolean isInSetupWizard() {
+        return mDevice.hasObject(By.res(getPackage(), "fre_pager"));
+    }
+
+    private boolean hasAccountRegistered() {
+        boolean addAcountTextPresent = mDevice.wait(Until.hasObject(By.textStartsWith("Add an " +
+                "account")), MAX_DIALOG_TRANSITION);
+
+        UiObject2 next = mDevice.wait(Until.findObject(
+                By.res(getPackage(), UI_POSITIVE_BUTTON_ID)), MAX_DIALOG_TRANSITION);
+        boolean signInButtonPresent =  next != null && "SIGN IN".equals(next.getText());
+
+        // If any of theese elements is present, then there is no account registered.
+        return !addAcountTextPresent && !signInButtonPresent;
+    }
+}
diff --git a/libraries/facebook-app-helper/Android.mk b/libraries/facebook-app-helper/Android.mk
new file mode 100644
index 0000000..b997aea
--- /dev/null
+++ b/libraries/facebook-app-helper/Android.mk
@@ -0,0 +1,23 @@
+#
+# 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.
+#
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := facebook-app-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator base-app-helpers
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/facebook-app-helper/src/android/platform/test/helpers/FacebookHelperImpl.java b/libraries/facebook-app-helper/src/android/platform/test/helpers/FacebookHelperImpl.java
new file mode 100644
index 0000000..b7f39f6
--- /dev/null
+++ b/libraries/facebook-app-helper/src/android/platform/test/helpers/FacebookHelperImpl.java
@@ -0,0 +1,285 @@
+/*
+ * 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.pm.PackageManager.NameNotFoundException;
+import android.os.SystemClock;
+import android.platform.test.helpers.exceptions.UnknownUiException;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.Until;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.util.Log;
+
+public class FacebookHelperImpl extends AbstractFacebookHelper {
+    private static final String TAG = "android.platform.test.helpers.FacebookHelperImpl";
+
+    private static final String UI_HOME_PAGE_CONTAINER_ID = "cs7";
+    private static final String UI_LOADING_VIEW_ID = "loading_view";
+    private static final String UI_LOGIN_BUTTON_ID = "bjb";
+    private static final String UI_LOGIN_PASSWORD_ID = "bj_";
+    private static final String UI_LOGIN_ROOT_ID = "bj6";
+    private static final String UI_LOGIN_USERNAME_ID = "bj8";
+    private static final String UI_NEWS_FEED_TAB_ID = "a0";
+    private static final String UI_NEWS_FEED_TAB_SELECTED_DESC = "News";
+    private static final String UI_PACKAGE_NAME = "com.facebook.katana";
+    private static final String UI_POST_BUTTON_ID = "rk";
+    private static final String UI_STATUS_TEXT_ID = "cmk";
+    private static final String UI_STATUS_UPDATE_BUTTON_ID = "bmp";
+    private static final String UI_LOGIN_ONE_TAP = "sc";
+
+    private static final long UI_LOGIN_WAIT = 30000;
+    private static final long UI_NAVIGATION_WAIT = 10000;
+
+    public FacebookHelperImpl(Instrumentation instr) {
+        super(instr);
+    }
+
+     /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void open() {
+        super.open();
+        mDevice.wait(Until.findObject(
+                By.res(UI_PACKAGE_NAME, UI_HOME_PAGE_CONTAINER_ID)), UI_NAVIGATION_WAIT);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getPackage() {
+        return UI_PACKAGE_NAME;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getLauncherName() {
+        return "Facebook";
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void dismissInitialDialogs() {
+
+    }
+
+    private UiObject2 getHomePageContainer() {
+        return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_HOME_PAGE_CONTAINER_ID));
+    }
+
+    private boolean isOnHomePage() {
+        return (getHomePageContainer() != null);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void scrollHomePage(Direction dir) {
+        UiObject2 scrollContainer = getHomePageContainer();
+        if (scrollContainer == null) {
+            throw new IllegalStateException("No valid scrolling mechanism found.");
+        }
+
+        scrollContainer.scroll(dir, 5.f);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void goToHomePage() {
+        // Try to go to the home page by repeatedly pressing the back button
+        for (int retriesRemaining = 5; retriesRemaining > 0 && !isOnHomePage();
+                --retriesRemaining) {
+            mDevice.pressBack();
+            mDevice.waitForIdle();
+        }
+    }
+
+    private UiObject2 getNewsFeedTab() {
+        return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_NEWS_FEED_TAB_ID));
+    }
+
+    private boolean isOnNewsFeed() {
+        UiObject2 newsFeedTab = getNewsFeedTab();
+        if (newsFeedTab == null) {
+            return false;
+        }
+
+        return newsFeedTab.getContentDescription().contains(UI_NEWS_FEED_TAB_SELECTED_DESC);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void goToNewsFeed() {
+        if (!isOnHomePage()) {
+            throw new IllegalStateException("Not on home page");
+        }
+
+        UiObject2 newsFeedTab = getNewsFeedTab();
+        if (newsFeedTab == null) {
+            throw new UnknownUiException("Could not find news feed tab");
+        }
+
+        newsFeedTab.click();
+        mDevice.wait(Until.findObject(By.res(UI_PACKAGE_NAME, UI_NEWS_FEED_TAB_ID).descContains(
+                UI_NEWS_FEED_TAB_SELECTED_DESC)), UI_NAVIGATION_WAIT);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void goToStatusUpdate() {
+        if (!isOnNewsFeed()) {
+            throw new IllegalStateException("Not on News Feed");
+        }
+
+        UiObject2 statusUpdateButton = null;
+        for (int retriesRemaining = 50; retriesRemaining > 0 && statusUpdateButton == null;
+                --retriesRemaining) {
+            scrollHomePage(Direction.UP);
+            statusUpdateButton = mDevice.findObject(
+                    By.res(UI_PACKAGE_NAME, UI_STATUS_UPDATE_BUTTON_ID));
+        }
+        if (statusUpdateButton == null) {
+            throw new UnknownUiException("Could not find status update button");
+        }
+
+        statusUpdateButton.click();
+        mDevice.wait(Until.findObject(
+                By.res(UI_PACKAGE_NAME, UI_STATUS_TEXT_ID)), UI_NAVIGATION_WAIT);
+
+        getStatusTextField().click();
+    }
+
+    private UiObject2 getStatusTextField() {
+        return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_STATUS_TEXT_ID));
+    }
+
+    private boolean isOnStatusUpdatePage() {
+        return (mDevice.hasObject(By.text("Post to Facebook")) &&
+                mDevice.hasObject(By.text("What's on your mind?")));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void clickStatusUpdateTextField() {
+        if (!isOnStatusUpdatePage()) {
+            throw new IllegalStateException("Not on status update page");
+        }
+
+        UiObject2 statusTextField = getStatusTextField();
+
+        if (statusTextField == null) {
+            throw new UnknownUiException("Cannot find status update text field");
+        }
+
+        statusTextField.click();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void setStatusText(String statusText) {
+        UiObject2 statusTextField = getStatusTextField();
+        if (statusTextField == null) {
+            throw new UnknownUiException("Could not find status text field");
+        }
+
+        statusTextField.setText(statusText);
+    }
+
+    private UiObject2 getPostButton() {
+        return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_POST_BUTTON_ID));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void postStatusUpdate() {
+        UiObject2 postButton = getPostButton();
+        if (postButton == null) {
+            throw new UnknownUiException("Could not find post status button");
+        }
+
+        postButton.click();
+        mDevice.wait(Until.findObject(
+                By.res(UI_PACKAGE_NAME, UI_HOME_PAGE_CONTAINER_ID)), UI_NAVIGATION_WAIT);
+    }
+
+    private boolean isOnLoginPage() {
+        return (mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_LOGIN_ROOT_ID)) != null);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void login(String username, String password) {
+        if (!isOnLoginPage()) {
+            return;
+        }
+
+        UiObject2 usernameTextField = mDevice.findObject(
+                By.res(UI_PACKAGE_NAME, UI_LOGIN_USERNAME_ID));
+        UiObject2 passwordTextField = mDevice.findObject(
+                By.res(UI_PACKAGE_NAME, UI_LOGIN_PASSWORD_ID));
+        UiObject2 loginButton = mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_LOGIN_BUTTON_ID));
+        if (usernameTextField == null) {
+            throw new UnknownUiException("Could not find username text field");
+        }
+        if (passwordTextField == null) {
+            throw new UnknownUiException("Could not find password text field");
+        }
+        if (loginButton == null) {
+            throw new UnknownUiException("Could not find login button");
+        }
+
+        usernameTextField.setText(username);
+        passwordTextField.setText(password);
+        loginButton.click();
+
+        // Check if one tap login screen is prompted and click on it
+        UiObject2 oneTapLogin = mDevice.wait(Until.findObject(
+                By.res(UI_PACKAGE_NAME, UI_LOGIN_ONE_TAP)), UI_NAVIGATION_WAIT);
+        if (oneTapLogin != null) {
+            oneTapLogin.click();
+        }
+
+        mDevice.wait(Until.findObject(
+                By.res(UI_PACKAGE_NAME, UI_HOME_PAGE_CONTAINER_ID)), UI_NAVIGATION_WAIT);
+        // Wait for user content to load after logging in
+        mDevice.wait(Until.gone(By.res(UI_PACKAGE_NAME, UI_LOADING_VIEW_ID)), UI_LOGIN_WAIT);
+    }
+}
diff --git a/libraries/flightdemo-app-helper/Android.mk b/libraries/flightdemo-app-helper/Android.mk
new file mode 100644
index 0000000..c6c6b84
--- /dev/null
+++ b/libraries/flightdemo-app-helper/Android.mk
@@ -0,0 +1,23 @@
+#
+# 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.
+#
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := flightdemo-app-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator base-app-helpers
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/flightdemo-app-helper/src/android/platform/test/helpers/FlightDemoHelperImpl.java b/libraries/flightdemo-app-helper/src/android/platform/test/helpers/FlightDemoHelperImpl.java
new file mode 100644
index 0000000..a8e069d
--- /dev/null
+++ b/libraries/flightdemo-app-helper/src/android/platform/test/helpers/FlightDemoHelperImpl.java
@@ -0,0 +1,123 @@
+/*
+ * 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.pm.PackageManager.NameNotFoundException;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.Until;
+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.UiSelector;
+import android.util.Log;
+
+import java.util.regex.Pattern;
+
+public class FlightDemoHelperImpl extends AbstractFlightDemoHelper {
+    private static final String LOG_TAG = FlightDemoHelperImpl.class.getCanonicalName();
+    private static final String UI_PACKAGE_NAME = "leofs.android.free";
+    private static final String UI_ACTIVITY_NAME = "leofs.android.free.LeofsActivity";
+
+    private static final int UI_RESPONSE_WAIT = 2000; // 2 secs
+    private static final int MAX_MENU_SCROLL_DOWN_COUNT = 10;
+
+    public FlightDemoHelperImpl(Instrumentation instr) {
+        super(instr);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getPackage() {
+        return UI_PACKAGE_NAME;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getLauncherName() {
+        return "Leo´s RC Simulator";
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void dismissInitialDialogs() {
+        // Nothing to do here.  There is no initial dialog in this app.
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void startDemo() {
+        Log.v(LOG_TAG, "Starting flight simulator demo");
+        selectMenuItem("Demo");
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void stopDemo() {
+        Log.v(LOG_TAG, "Stopping flight simulator demo");
+        selectMenuItem("Reset");
+        mDevice.pressBack();
+    }
+
+    private void selectMenuItem(String item) {
+        mDevice.pressMenu();
+        UiObject2 container = mDevice.wait(Until.findObject(By.res("android", "list")),
+                                           UI_RESPONSE_WAIT);
+        if (container == null) {
+            throw new IllegalStateException("Cannot find scrollable menu");
+        }
+
+        String err_msg = String.format("Cannot find menu item %s", item);
+        int scroll_counter = 0;
+        UiObject2 button = null;
+        boolean reachedEnd = false;
+        while (!reachedEnd) {
+            final Pattern word = Pattern.compile(item, Pattern.CASE_INSENSITIVE);
+            button = mDevice.wait(Until.findObject(By.text(word)), UI_RESPONSE_WAIT);
+            if (button != null) {
+                button.click();
+                break;
+            }
+
+            if (!container.scroll(Direction.DOWN, 1.0f) &&
+                scroll_counter >= MAX_MENU_SCROLL_DOWN_COUNT) {
+                reachedEnd = true;
+            }
+            scroll_counter++;
+        }
+        if (button != null) {
+            button.click();
+        }
+        else {
+            throw new IllegalStateException(err_msg);
+        }
+    }
+}
diff --git a/libraries/gmail-app-helper/Android.mk b/libraries/gmail-app-helper/Android.mk
new file mode 100644
index 0000000..d9db92c
--- /dev/null
+++ b/libraries/gmail-app-helper/Android.mk
@@ -0,0 +1,23 @@
+#
+# 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.
+#
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := gmail-app-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator base-app-helpers
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/gmail-app-helper/src/android/platform/test/helpers/GmailHelperImpl.java b/libraries/gmail-app-helper/src/android/platform/test/helpers/GmailHelperImpl.java
new file mode 100644
index 0000000..98482c2
--- /dev/null
+++ b/libraries/gmail-app-helper/src/android/platform/test/helpers/GmailHelperImpl.java
@@ -0,0 +1,654 @@
+/*
+ * 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.pm.PackageManager.NameNotFoundException;
+import android.content.res.Configuration;
+import android.os.SystemClock;
+import android.platform.test.helpers.exceptions.UnknownUiException;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.Until;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.util.Log;
+import android.webkit.WebView;
+import android.widget.ListView;
+import android.widget.ImageButton;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.regex.Pattern;
+
+public class GmailHelperImpl extends AbstractGmailHelper {
+    private static final String LOG_TAG = GmailHelperImpl.class.getSimpleName();
+
+    private static final long APP_INIT_WAIT = 10000;
+    private static final long DIALOG_TIMEOUT = 5000;
+    private static final long POPUP_TIMEOUT = 7500;
+    private static final long COMPOSE_TIMEOUT = 10000;
+    private static final long SEND_TIMEOUT = 10000;
+    private static final long LOADING_TIMEOUT = 25000;
+    private static final long LOAD_EMAIL_TIMEOUT = 20000;
+    private static final long WIFI_TIMEOUT = 60 * 1000;
+    private static final long RELOAD_INBOX_TIMEOUT = 10 * 1000;
+    private static final long COMPOSE_EMAIL_TIMEOUT = 10 * 1000;
+
+    private static final String UI_ATTACHMENT_TILE_SAVE_ID = "attachment_tile_save";
+    private static final String UI_NAME_ID = "name";
+    private static final String UI_PACKAGE_NAME = "com.google.android.gm";
+    private static final String UI_PROMO_ACTION_NEG_RES = "promo_action_negative_single_line";
+    private static final String UI_CONVERSATIONS_LIST_ID = "conversation_list_view";
+    private static final String UI_CONVERSATION_LIST_LOADING_VIEW_ID =
+            "conversation_list_loading_view";
+    private static final String UI_CONVERSATION_PAGER = "conversation_pager";
+    private static final String UI_MULTI_PANE_CONTAINER_ID = "two_pane_activity";
+    private static final BySelector PRIMARY_SELECTOR =
+            By.res(UI_PACKAGE_NAME, "name").text("Primary");
+    private static final BySelector INBOX_SELECTOR =
+            By.res(UI_PACKAGE_NAME, "name").text("Inbox");
+    private static final BySelector NAV_DRAWER_SELECTOR = By.res("android", "list").focused(true);
+
+    public GmailHelperImpl(Instrumentation instr) {
+        super(instr);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getPackage() {
+        return "com.google.android.gm";
+    }
+
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getLauncherName() {
+        return "Gmail";
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void dismissInitialDialogs() {
+        // Check for the first, option dialog dismissal screen
+        if (mDevice.wait(Until.hasObject(By.res(UI_PACKAGE_NAME, "welcome_tour_pager")),
+                APP_INIT_WAIT)) {
+            // Dismiss "New in Gmail" with GOT IT button or "Wecome to Gmail" with > button
+            BySelector gotItSelector = By.res(UI_PACKAGE_NAME, "welcome_tour_got_it");
+            BySelector skipSelector = By.res(UI_PACKAGE_NAME, "welcome_tour_skip");
+            if (mDevice.hasObject(gotItSelector)) {
+                mDevice.findObject(gotItSelector).clickAndWait(Until.newWindow(), DIALOG_TIMEOUT);
+            } else if (mDevice.hasObject(skipSelector)) {
+                mDevice.findObject(skipSelector).clickAndWait(Until.newWindow(), DIALOG_TIMEOUT);
+            }
+        } else {
+            Log.e(LOG_TAG, "Unable to find initial screen. Continuing anyway.");
+        }
+        // Dismiss "Add another email address" with TAKE ME TO GMAIL button
+        UiObject2 tutorialDone = mDevice.wait(Until.findObject(
+                By.res(UI_PACKAGE_NAME, "action_done")), DIALOG_TIMEOUT);
+        if (tutorialDone != null) {
+            tutorialDone.clickAndWait(Until.newWindow(), DIALOG_TIMEOUT);
+        }
+        // Dismiss dogfood confidentiality dialog with OK, GOT IT button
+        Pattern gotItWord = Pattern.compile("OK, GOT IT", Pattern.CASE_INSENSITIVE);
+        UiObject2 splash = mDevice.wait(Until.findObject(By.text(gotItWord)), DIALOG_TIMEOUT);
+        if (splash != null) {
+            splash.clickAndWait(Until.newWindow(), DIALOG_TIMEOUT);
+        }
+        // Wait for "Getting your messages" to disappear
+        if (mDevice.findObject(By.textContains("Getting your messages")) != null) {
+            if (!mDevice.wait(Until.gone(By.text("Getting your messages")), WIFI_TIMEOUT)) {
+                throw new UnknownUiException(
+                        "Timed out waiting for 'Getting your messages' to disappear");
+            }
+        }
+        if (!mDevice.wait(Until.hasObject(
+                By.res(UI_PACKAGE_NAME, UI_CONVERSATIONS_LIST_ID)), WIFI_TIMEOUT)) {
+            throw new UnknownUiException("Timed out waiting for conversation list to appear");
+        }
+        // Dismiss "Tap a sender image" dialog
+        UiObject2 senderImageDismissButton =
+                mDevice.findObject(By.res(UI_PACKAGE_NAME, "dismiss_icon"));
+        if (senderImageDismissButton != null) {
+            senderImageDismissButton.click();
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void goToInbox() {
+        // Check if already in Inbox or Primary
+        if (isInPrimaryOrInbox()) {
+            return;
+        }
+
+        if (isMultiPaneActivity()) {
+            // Select for the closed Primary icon
+            UiObject2 primaryClosed = mDevice.findObject(
+                    By.res(UI_PACKAGE_NAME, "image_view").text("Primary"));
+            if (primaryClosed != null) {
+                primaryClosed.click();
+                mDevice.waitForIdle();
+                return;
+            }
+
+            // Select for the closed Inbox icon
+            UiObject2 inboxClosed = mDevice.findObject(
+                    By.res(UI_PACKAGE_NAME, "image_view").text("Inbox"));
+            if (inboxClosed != null) {
+                inboxClosed.click();
+                mDevice.waitForIdle();
+                return;
+            }
+
+            scrollNavigationDrawer(Direction.UP);
+
+            // Select for the open Primary icon
+            UiObject2 primaryOpen = mDevice.findObject(
+                    By.res(UI_PACKAGE_NAME, "name").text("Primary"));
+            if (primaryOpen != null) {
+                primaryOpen.click();
+                mDevice.waitForIdle();
+                return;
+            }
+
+            // Select for the open Inbox icon
+            UiObject2 inboxOpen = mDevice.findObject(
+                    By.res(UI_PACKAGE_NAME, "name").text("Inbox"));
+            if (inboxOpen != null) {
+                inboxOpen.click();
+                mDevice.waitForIdle();
+                return;
+            }
+
+            // Currently unhandled case; throw Exception.
+            throw new RuntimeException("Unable to find method to get to Primary/Inbox");
+        } else {
+            // Simply press back if in a conversation
+            if (isInConversation()) {
+                mDevice.pressBack();
+                waitForConversationsList();
+            }
+
+            // If in another e-mail sub-folder, go to Primary or Inbox
+            if (!isInPrimaryOrInbox()) {
+                // Search with the navigation drawer
+                openNavigationDrawer();
+
+                // Select for "Primary" and for "Inbox"
+                UiObject2 primaryInboxSelector = mDevice.findObject(PRIMARY_SELECTOR);
+                if (primaryInboxSelector == null) {
+                    primaryInboxSelector = mDevice.findObject(INBOX_SELECTOR);
+                }
+
+                primaryInboxSelector.click();
+                waitForConversationsList();
+            }
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void goToComposeEmail() {
+        if (!isInPrimaryOrInbox()) {
+            throw new IllegalStateException("Gmail is not on the Inbox or Primary page");
+        }
+        UiObject2 compose = mDevice.findObject(By.desc("Compose"));
+        if (compose == null) {
+            throw new UnknownUiException("Compose button not found");
+        }
+        compose.clickAndWait(Until.newWindow(), COMPOSE_TIMEOUT);
+        waitForCompose();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void openEmailByIndex(int index) {
+        if (!isInMailbox()) {
+            throw new IllegalStateException("Must be in a mailbox to open an email by index");
+        }
+
+        if (index >= getVisibleEmailCount()) {
+            throw new IllegalArgumentException(String.format("Cannot select %s'th message of %s",
+                    (index + 1), getVisibleEmailCount()));
+        }
+
+        // Select an e-mail by index
+        UiObject2 conversationList = getConversationList();
+        List<UiObject2> emails = conversationList.findObjects(
+                By.clazz(android.widget.FrameLayout.class));
+        if (conversationList == null) {
+            throw new UnknownUiException("No e-mails found.");
+        }
+        emails.get(index).click();
+
+        // Wait until the e-mail is open
+        UiObject2 loadMsg = mDevice.findObject(By.res(UI_PACKAGE_NAME, "loading_progress"));
+        if (loadMsg != null) {
+            if (!mDevice.wait(Until.gone(
+                    By.res(UI_PACKAGE_NAME, "loading_progress")), LOADING_TIMEOUT)) {
+                throw new RuntimeException("Loading message timed out after 20s");
+            }
+        }
+
+        waitForConversation();
+    }
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int getVisibleEmailCount() {
+        if (!isInMailbox()) {
+            throw new IllegalStateException("Must be in a mailbox to open an email by index");
+        }
+
+        return getConversationList().getChildCount();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void sendReplyEmail(String address, String body) {
+        if (!isInConversation()) {
+            throw new IllegalStateException("Must have an e-mail open to send a reply.");
+        }
+
+        UiObject2 convScroll = getConversationPager();
+        while(convScroll.scroll(Direction.DOWN, 1.0f));
+
+        UiObject2 replyButton = mDevice.findObject(By.text("Reply"));
+        if (replyButton != null) {
+            replyButton.clickAndWait(Until.newWindow(), COMPOSE_TIMEOUT);
+            waitForCompose();
+        } else {
+            throw new UnknownUiException("Failed to find a 'Reply' button.");
+        }
+
+        // Set the necessary fields (address and body)
+        setEmailToAddress(address);
+        setEmailBody(body);
+
+        // Send the reply e-mail and wait for original e-mail
+        clickSendButton();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void setEmailToAddress(String address) {
+        UiObject2 convScroll = getComposeScrollContainer();
+        while (convScroll.scroll(Direction.UP, 1.0f));
+
+        UiObject2 toField = getToField();
+        for (int retries = 5; retries > 0 && toField == null; retries--) {
+            convScroll.scroll(Direction.DOWN, 1.0f);
+            toField = getToField();
+        }
+
+        if (toField != null) {
+            toField.setText(address);
+        } else {
+            throw new UnknownUiException("Failed to find a 'To' field.");
+        }
+    }
+
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void setEmailSubject(String subject) {
+        UiObject2 convScroll = getComposeScrollContainer();
+        while (convScroll.scroll(Direction.UP, 1.0f));
+
+        UiObject2 subjectField = getSubjectField();
+        for (int retries = 5; retries > 0 && subjectField == null; retries--) {
+            convScroll.scroll(Direction.DOWN, 1.0f);
+            subjectField = getSubjectField();
+        }
+
+        if (subjectField != null) {
+            subjectField.setText(subject);
+        } else {
+            throw new UnknownUiException("Failed to find a 'Subject' field.");
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void setEmailBody(String body) {
+        UiObject2 convScroll = getComposeScrollContainer();
+        while (convScroll.scroll(Direction.UP, 1.0f));
+
+        UiObject2 bodyField = getBodyField();
+        for (int retries = 5; retries > 0 && bodyField == null; retries--) {
+            convScroll.scroll(Direction.DOWN, 1.0f);
+            bodyField = getBodyField();
+        }
+
+        if (bodyField != null) {
+            // Ensure the focus is left in the body field.
+            bodyField.click();
+            bodyField.setText(body);
+        } else {
+            throw new UnknownUiException("Failed to find a 'Body' field.");
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void clickSendButton() {
+        UiObject2 convScroll = getComposeScrollContainer();
+        while (convScroll.scroll(Direction.UP, 1.0f));
+
+        UiObject2 sendButton = getSendButton();
+        if (sendButton != null) {
+            sendButton.clickAndWait(Until.newWindow(), SEND_TIMEOUT);
+            waitForConversation();
+        } else {
+            throw new UnknownUiException("Failed to find a 'Send' button.");
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getComposeEmailBody(){
+        UiObject2 bodyField = getBodyField();
+        return bodyField.getText();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void openNavigationDrawer() {
+        for (int retries = 3; retries > 0; retries--) {
+            if (isNavDrawerOpen()) {
+                return;
+            }
+
+            UiObject2 nav = mDevice.findObject(By.desc(Pattern.compile(
+                    "(Open navigation drawer)|(Navigate up)")));
+
+            if (nav == null) {
+                throw new IllegalStateException("Could not find navigation drawer");
+            }
+            nav.click();
+            mDevice.waitForIdle();
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void scrollNavigationDrawer(Direction dir) {
+        if (dir == Direction.RIGHT || dir == Direction.LEFT) {
+            throw new IllegalArgumentException("Can only scroll navigation drawer up and down.");
+        }
+
+        UiObject2 scroll = getNavDrawerContainer();
+        if (scroll == null) {
+            throw new UnknownUiException("No navigation drawer found to scroll");
+        }
+        scroll.scroll(dir, 1.0f);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean closeNavigationDrawer() {
+        UiObject2 navDrawer = mDevice.wait(Until.findObject(
+                By.clazz(ImageButton.class).desc("Close navigation drawer")), 1000);
+        if (navDrawer != null) {
+            navDrawer.click();
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean isInComposeEmail(){
+        return mDevice.findObject(By.res(UI_PACKAGE_NAME, "compose")) != null;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean isInPrimaryOrInbox() {
+        if (isMultiPaneActivity()) {
+            return (mDevice.hasObject(By.res(UI_PACKAGE_NAME, "actionbar_title").text("Primary")) ||
+                    mDevice.hasObject(By.res(UI_PACKAGE_NAME, "actionbar_title").text("Inbox")));
+        } else {
+            return getConversationList() != null &&
+                    (mDevice.hasObject(By.text("Primary")) ||
+                            mDevice.hasObject(By.text("Inbox")));
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void scrollMailbox(Direction direction, float amount, boolean scrollToEnd) {
+        if (!isInMailbox()) {
+            throw new IllegalStateException("Not in mailbox");
+        }
+
+        if (!(Direction.UP.equals(direction) || Direction.DOWN.equals(direction))) {
+            throw new IllegalArgumentException("Scroll direction must be UP or DOWN");
+        }
+
+        UiObject2 scrollContainer = getConversationList();
+        if (scrollContainer == null) {
+            throw new IllegalStateException("Could not find scroll container");
+        }
+
+        scroll(scrollContainer, direction, amount, scrollToEnd);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void scrollEmail(Direction direction, float amount, boolean scrollToEnd) {
+        if (!(Direction.UP.equals(direction) || Direction.DOWN.equals(direction))) {
+            throw new IllegalArgumentException("Scroll direction must be UP or DOWN");
+        }
+
+        UiObject2 scrollContainer = getConversationPager();
+        if (scrollContainer == null) {
+            throw new IllegalStateException("Could not find email scroll container");
+        }
+
+        scroll(scrollContainer, direction, amount, scrollToEnd);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void openMailbox(String mailboxName) {
+        if (!isNavDrawerOpen()) {
+            throw new IllegalStateException("Navigation drawer is not open");
+        }
+
+        UiObject2 mailbox = null;
+        for (int scrollsRemaining = 5; scrollsRemaining > 0; --scrollsRemaining) {
+            mailbox = mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_NAME_ID).text(
+                    Pattern.compile(mailboxName, Pattern.CASE_INSENSITIVE)));
+            if (mailbox != null) {
+                break;
+            } else {
+                scrollNavigationDrawer(Direction.DOWN);
+            }
+        }
+        if (mailbox == null) {
+            throw new IllegalArgumentException(
+                    String.format("Could not find mailbox '%s'", mailboxName));
+        }
+        mailbox.click();
+        mDevice.waitForIdle();
+        mDevice.wait(Until.gone(
+                By.res(UI_PACKAGE_NAME, UI_CONVERSATION_LIST_LOADING_VIEW_ID)), WIFI_TIMEOUT);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void returnToMailbox() {
+        for (int retriesRemaining = 5; retriesRemaining > 0; --retriesRemaining) {
+            if (isInMailbox()) {
+                break;
+            } else {
+                mDevice.pressBack();
+                mDevice.waitForIdle();
+            }
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void downloadAttachment(int index) {
+        if (!isInConversation()) {
+            throw new IllegalStateException("Email is not open");
+        }
+
+        List<UiObject2> downloadButtons =
+                mDevice.findObjects(By.res(UI_PACKAGE_NAME, UI_ATTACHMENT_TILE_SAVE_ID));
+        if (downloadButtons != null && index >= 0 && index < downloadButtons.size()) {
+            downloadButtons.get(index).click();
+        } else {
+            throw new IndexOutOfBoundsException("attachment index out of bounds");
+        }
+    }
+
+    private UiObject2 getToField() {
+        return mDevice.findObject(By.res(UI_PACKAGE_NAME, "to"));
+    }
+
+    private UiObject2 getSubjectField() {
+        return mDevice.findObject(By.res(UI_PACKAGE_NAME, "subject"));
+    }
+
+    private UiObject2 getBodyField() {
+        return mDevice.findObject(By.res(UI_PACKAGE_NAME, "body"));
+    }
+
+    private UiObject2 getSendButton() {
+        return mDevice.findObject(By.desc("Send"));
+    }
+
+    private UiObject2 getComposeScrollContainer() {
+        return mDevice.findObject(By.res(UI_PACKAGE_NAME, "compose"));
+    }
+
+    private UiObject2 getNavDrawerContainer() {
+        return mDevice.findObject(NAV_DRAWER_SELECTOR);
+    }
+
+    private UiObject2 getConversationList() {
+        return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_CONVERSATIONS_LIST_ID));
+    }
+
+    private UiObject2 getConversationPager() {
+        return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_CONVERSATION_PAGER));
+    }
+
+    private boolean isInConversation() {
+        return mDevice.hasObject(By.res(UI_PACKAGE_NAME, UI_CONVERSATION_PAGER));
+    }
+
+    private boolean isNavDrawerOpen() {
+        if (isMultiPaneActivity()) {
+            return mDevice.hasObject(By.res("android", "list"));
+        } else {
+            return mDevice.hasObject(NAV_DRAWER_SELECTOR);
+        }
+    }
+
+    private void waitForConversationsList() {
+        mDevice.wait(Until.hasObject(
+                By.res(UI_PACKAGE_NAME, UI_CONVERSATIONS_LIST_ID)), RELOAD_INBOX_TIMEOUT);
+    }
+
+    private void waitForConversation() {
+        mDevice.wait(Until.hasObject(
+                By.res(UI_PACKAGE_NAME, UI_CONVERSATION_PAGER)), LOAD_EMAIL_TIMEOUT);
+    }
+
+    private void waitForCompose() {
+        mDevice.wait(Until.findObject(By.res(UI_PACKAGE_NAME, "compose")), COMPOSE_EMAIL_TIMEOUT);
+    }
+
+    private boolean isMultiPaneActivity() {
+        return mDevice.hasObject(By.res(UI_PACKAGE_NAME, UI_MULTI_PANE_CONTAINER_ID));
+    }
+
+    private boolean isInMailbox() {
+        if (isMultiPaneActivity()) {
+            return mDevice.hasObject(By.desc("Search"));
+        } else {
+            return mDevice.hasObject(By.desc("Search")) && getNavDrawerContainer() == null;
+        }
+    }
+
+    private void scroll(UiObject2 scrollContainer, Direction direction,
+            float amount, boolean scrollToEnd) {
+        if (amount < 0.0f) {
+            throw new IllegalArgumentException("Scroll amount cannot be negative");
+        }
+
+        if (scrollToEnd) {
+            while (scrollContainer.scroll(direction, 1.0f)) {
+                // empty
+            }
+        } else {
+            scrollContainer.scroll(direction, (float) amount);
+        }
+    }
+}
diff --git a/libraries/google-app-camera-helper/Android.mk b/libraries/google-app-camera-helper/Android.mk
new file mode 100644
index 0000000..2aa862e
--- /dev/null
+++ b/libraries/google-app-camera-helper/Android.mk
@@ -0,0 +1,23 @@
+#
+# 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.
+#
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := google-camera-app-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator base-app-helpers launcher-helper-lib
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/google-app-camera-helper/src/android/platform/test/helpers/GoogleCameraHelperImpl.java b/libraries/google-app-camera-helper/src/android/platform/test/helpers/GoogleCameraHelperImpl.java
new file mode 100644
index 0000000..3a559ae
--- /dev/null
+++ b/libraries/google-app-camera-helper/src/android/platform/test/helpers/GoogleCameraHelperImpl.java
@@ -0,0 +1,1201 @@
+/*
+ * 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.platform.test.helpers;
+
+import android.app.Instrumentation;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.SystemClock;
+import android.platform.test.helpers.exceptions.UnknownUiException;
+import android.support.test.launcherhelper.ILauncherStrategy;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Configurator;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.Until;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.UiWatcher;
+import android.util.Log;
+
+import java.text.SimpleDateFormat;
+import java.text.DateFormat;
+import java.util.Date;
+import java.util.regex.Pattern;
+
+public class GoogleCameraHelperImpl extends AbstractGoogleCameraHelper {
+    private static final String LOG_TAG = GoogleCameraHelperImpl.class.getSimpleName();
+    private static final String UI_ACTIVITY_VIEW_ID = "activity_root_view";
+    private static final String UI_ALBUM_FILMSTRIP_VIEW_ID = "filmstrip_view";
+    private static final String UI_PACKAGE_NAME = "com.android.camera2";
+    private static final String UI_RECORDING_TIME_ID = "recording_time";
+    private static final String UI_SHUTTER_DESC_CAM_3X = "Capture photo";
+    private static final String UI_SHUTTER_DESC_CAM_2X = "Shutter";
+    private static final String UI_SHUTTER_DESC_VID_3X = "Capture video";
+    private static final String UI_SHUTTER_DESC_VID_2X = "Shutter";
+    private static final String UI_THUMBNAIL_ALBUM_BUTTON_ID = "rounded_thumbnail_view";
+    private static final String UI_TOGGLE_BUTTON_ID = "photo_video_paginator";
+    private static final String UI_BACK_FRONT_TOGGLE_BUTTON_ID = "camera_toggle_button";
+    private static final String UI_MODE_OPTION_TOGGLE_BUTTON_ID = "mode_options_toggle";
+    private static final String UI_SHUTTER_BUTTON_ID_3X = "photo_video_button";
+    private static final String UI_SHUTTER_BUTTON_ID_2X = "shutter_button";
+    private static final String UI_SETTINGS_BUTTON_ID = "settings_button";
+    private static final String UI_MENU_BUTTON_ID_3X = "menuButton";
+    private static final String UI_MENU_BUTTON_ID_4X = "toybox_menu_button";
+    private static final String UI_SPECIAL_MODE_CLOSE = "closeButton";
+    private static final String UI_HDR_BUTTON_ID_2X = "hdr_plus_toggle_button";
+    private static final String UI_HDR_BUTTON_ID_3X = "hdr_plus_toggle_button";
+    private static final String UI_HDR_BUTTON_ID_4X = "hdr_button";
+    private static final String UI_HDR_AUTO_ID_4X = "hdr_auto";
+    private static final String UI_HDR_ON_ID_4X = "hdr_on";
+    private static final String UI_HDR_OFF_ID_4X = "hdr_off";
+    private static final String UI_SELECTED_OPTION_ID = "selected_option_label";
+    private static final String UI_HFR_TOGGLE_ID_J = "hfr_button";
+    private static final String UI_HFR_TOGGLE_ID_I = "hfr_mode_toggle_button";
+
+    private static final String DESC_HDR_AUTO = "HDR Plus auto";
+    private static final String DESC_HDR_OFF_3X = "HDR Plus off";
+    private static final String DESC_HDR_ON_3X = "HDR Plus on";
+
+    private static final String DESC_HDR_OFF_2X = "HDR off";
+    private static final String DESC_HDR_ON_2X = "HDR on";
+
+    private static final String DESC_HFR_OFF = "Slow motion is off";
+    private static final String DESC_HFR_120_FPS = "Slow motion is set to 120 fps";
+    private static final String DESC_HFR_240_FPS = "Slow motion is set to 240 fps";
+
+    private static final String TEXT_4K_ON = "UHD 4K";
+    private static final String TEXT_HD_1080 = "HD 1080p";
+    private static final String TEXT_HD_720 = "HD 720p";
+    private static final String TEXT_HDR_AUTO = "HDR off";
+    private static final String TEXT_HDR_ON = "HDR+ Auto";
+    private static final String TEXT_HDR_OFF = "HDR on";
+    private static final String TEXT_BACK_VIDEO_RESOLUTION_4X = "Back camera video resolution";
+    private static final String TEXT_BACK_VIDEO_RESOLUTION_3X = "Back camera video";
+
+    public static final int HDR_MODE_AUTO = -1;
+    public static final int HDR_MODE_OFF = 0;
+    public static final int HDR_MODE_ON = 1;
+
+    public static final int VIDEO_4K_MODE_ON = 1;
+    public static final int VIDEO_HD_1080 = 0;
+    public static final int VIDEO_HD_720 = -1;
+
+    public static final int HFR_MODE_OFF = 0;
+    public static final int HFR_MODE_120_FPS = 1;
+    public static final int HFR_MODE_240_FPS = 2;
+
+    private static final long APP_INIT_WAIT = 20000;
+    private static final long DIALOG_TRANSITION_WAIT = 5000;
+    private static final long SHUTTER_WAIT_TIME = 20000;
+    private static final long SWITCH_WAIT_TIME = 5000;
+    private static final long MENU_WAIT_TIME = 5000;
+
+    private boolean mIsVersionH = false;
+    private boolean mIsVersionI = false;
+    private boolean mIsVersionJ = false;
+    private boolean mIsVersionK = false;
+
+    public GoogleCameraHelperImpl(Instrumentation instr) {
+        super(instr);
+
+        try {
+            mIsVersionH = getVersion().startsWith("2.");
+            mIsVersionI = getVersion().startsWith("3.0") || getVersion().startsWith("3.1");
+            mIsVersionJ = getVersion().startsWith("3.2");
+            mIsVersionK = getVersion().startsWith("4");
+        } catch (NameNotFoundException e) {
+            Log.e(LOG_TAG, String.format("Unable to find package by name, %s", getPackage()));
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getPackage() {
+        return "com.google.android.GoogleCamera";
+    }
+
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getLauncherName() {
+        return "Camera";
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void dismissInitialDialogs() {
+        if (mIsVersionK) {
+            // Dismiss dogfood confidentiality dialog
+            Pattern okText = Pattern.compile("OK, GOT IT", Pattern.CASE_INSENSITIVE);
+            UiObject2 dogfoodMessage = mDevice.wait(
+                    Until.findObject(By.text(okText)), APP_INIT_WAIT);
+            if (dogfoodMessage != null) {
+                dogfoodMessage.click();
+            }
+        } else if (mIsVersionI || mIsVersionJ) {
+            // Dismiss dogfood confidentiality dialog
+            Pattern okText = Pattern.compile("OK, GOT IT", Pattern.CASE_INSENSITIVE);
+            UiObject2 dogfoodMessage = mDevice.wait(
+                    Until.findObject(By.text(okText)), APP_INIT_WAIT);
+            if (dogfoodMessage != null) {
+                dogfoodMessage.click();
+            }
+            // Swipe left to dismiss 'how to open video message'
+            UiObject2 activityView = mDevice.wait(Until.findObject(
+                    By.res(UI_PACKAGE_NAME, "activity_root_view")), DIALOG_TRANSITION_WAIT);
+            if (activityView != null) {
+                activityView.swipe(Direction.LEFT, 1.0f);
+            }
+            // Confirm 'GOT IT' for action above
+            UiObject2 thanks = mDevice.wait(Until.findObject(By.text("GOT IT")),
+                    DIALOG_TRANSITION_WAIT);
+            if (thanks != null) {
+                thanks.click();
+            }
+        } else {
+            BySelector confirm = By.res(UI_PACKAGE_NAME, "confirm_button");
+            UiObject2 location = mDevice.wait(Until.findObject(
+                    By.copy(confirm).text("NEXT")), APP_INIT_WAIT);
+            if (location != null) {
+                location.click();
+            }
+            // Choose sensor size. It's okay to timeout. These dialog screens might not exist..
+            UiObject2 sensor = mDevice.wait(Until.findObject(
+                    By.copy(confirm).text("OK, GOT IT")), DIALOG_TRANSITION_WAIT);
+            if (sensor != null) {
+                sensor.click();
+            }
+            // Dismiss dogfood dialog
+            if (mDevice.wait(Until.hasObject(
+                    By.res(UI_PACKAGE_NAME, "internal_release_dialog_title")), 5000)) {
+                mDevice.findObject(By.res(UI_PACKAGE_NAME, "ok_button")).click();
+            }
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void capturePhoto() {
+        if (!isCameraMode()) {
+            throw new IllegalStateException(
+                    "GoogleCamera must be in Camera mode to capture photos.");
+        }
+
+        getCameraShutter().click();
+        waitForCameraShutterEnabled();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void captureVideo(long timeInMs) {
+        if (!isVideoMode()) {
+            throw new IllegalStateException("GoogleCamera must be in Video mode to record videos.");
+        }
+
+        if (isRecording()) {
+            return;
+        }
+
+        // Temporary hack #1: Make UI code responsive by shortening the UiAutomator idle timeout.
+        // The pulsing record button broadcasts unnecessary events of TYPE_WINDOW_CONTENT_CHANGED,
+        // but we intend to have a fix and remove this hack with Kenai (GC 3.0).
+        long original = Configurator.getInstance().getWaitForIdleTimeout();
+        Configurator.getInstance().setWaitForIdleTimeout(1000);
+
+        try {
+            getVideoShutter().click();
+            SystemClock.sleep(timeInMs);
+            getVideoShutter().click();
+            waitForVideoShutterEnabled();
+        } finally {
+            Configurator.getInstance().setWaitForIdleTimeout(original);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void snapshotVideo(long videoTimeInMs, long snapshotStartTimeInMs) {
+        if (!isVideoMode()) {
+            throw new IllegalStateException("GoogleCamera must be in Video mode to record videos.");
+        } else if (videoTimeInMs <= snapshotStartTimeInMs) {
+            throw new IllegalArgumentException(
+                    "video recording time length must be larger than snapshot start time");
+        }
+
+        // Temporary hack #2: Make UI code responsive by shortening the UiAutomator idle timeout.
+        // The pulsing record button broadcasts unnecessary events of TYPE_WINDOW_CONTENT_CHANGED,
+        // but we intend to have a fix and remove this hack with Kenai (GC 3.0).
+        long original = Configurator.getInstance().getWaitForIdleTimeout();
+        Configurator.getInstance().setWaitForIdleTimeout(1000);
+
+        if (isRecording()) {
+            return;
+        }
+
+        try {
+            getVideoShutter().click();
+            SystemClock.sleep(snapshotStartTimeInMs);
+
+            boolean snapshot_success = false;
+
+            // Take a snapshot
+            if (mIsVersionJ || mIsVersionK) {
+                UiObject2 snapshotButton = mDevice.findObject(By.res(UI_PACKAGE_NAME, "snapshot_button"));
+                if (snapshotButton != null) {
+                    snapshotButton.click();
+                    snapshot_success = true;
+                }
+            } else if (mIsVersionI) {
+                // Ivvavik Version of GCA doesn't support snapshot
+                snapshot_success = false;
+            } else {
+                UiObject2 snapshotButton = mDevice.findObject(By.res(UI_PACKAGE_NAME, "recording_time"));
+                if (snapshotButton != null) {
+                    snapshotButton.click();
+                    snapshot_success = true;
+                }
+            }
+
+            if (!snapshot_success) {
+                getVideoShutter().click();
+                waitForVideoShutterEnabled();
+                throw new UnknownUiException("snapshot button not found!");
+            }
+
+            SystemClock.sleep(videoTimeInMs - snapshotStartTimeInMs);
+            getVideoShutter().click();
+            waitForVideoShutterEnabled();
+        } finally {
+            Configurator.getInstance().setWaitForIdleTimeout(original);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void goToCameraMode() {
+        if (isCameraMode()) {
+            return;
+        }
+
+        if (mIsVersionI || mIsVersionJ || mIsVersionK) {
+            UiObject2 toggle = getCameraVideoToggleButton();
+            if (toggle != null) {
+                toggle.click();
+            }
+        } else {
+            openMenu();
+            selectMenuItem("Camera");
+        }
+
+        mDevice.waitForIdle();
+        waitForCameraShutterEnabled();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void goToVideoMode() {
+        if (isVideoMode()) {
+            return;
+        }
+
+        if (mIsVersionI || mIsVersionJ || mIsVersionK) {
+            UiObject2 toggle = getCameraVideoToggleButton();
+            if (toggle != null) {
+                toggle.click();
+            }
+        } else {
+            openMenu();
+            selectMenuItem("Video");
+        }
+
+        mDevice.waitForIdle();
+        waitForVideoShutterEnabled();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void goToBackCamera() {
+        if (isBackCamera()) {
+            return;
+        }
+
+        // Close menu if open
+        closeMenu();
+
+        if (mIsVersionI || mIsVersionJ || mIsVersionK) {
+            pressBackFrontToggleButton();
+        } else {
+            // Open mode options if not open.
+            // Note: the mode option button only appear if mode option menu not open
+            UiObject2 modeoptions = getModeOptionsMenuButton();
+            if (modeoptions != null) {
+                modeoptions.click();
+            }
+            pressBackFrontToggleButton();
+        }
+
+        // Wait for ensuring back camera button enabled
+        waitForBackEnabled();
+
+        // Wait for ensuring shutter button enabled
+        waitForCurrentShutterEnabled();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void goToFrontCamera() {
+        if (isFrontCamera()) {
+            return;
+        }
+
+        // Close menu if open
+        closeMenu();
+
+        if (mIsVersionI || mIsVersionJ || mIsVersionK) {
+            pressBackFrontToggleButton();
+        } else {
+            // Open mode options if not open.
+            // Note: the mode option button only appear if mode option menu not open
+            UiObject2 modeoptions = getModeOptionsMenuButton();
+            if (modeoptions != null) {
+                modeoptions.click();
+            }
+            pressBackFrontToggleButton();
+        }
+
+        // Wait for ensuring front camera button enabled
+        waitForFrontEnabled();
+
+        // Wait for ensuring shutter button enabled
+        waitForCurrentShutterEnabled();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void setHdrMode(int mode) {
+        if (!isCameraMode()) {
+            throw new IllegalStateException("Cannot set HDR unless in camera mode.");
+        }
+
+        if (mIsVersionK) {
+            if (getHdrToggleButton() == null) {
+                if (mode == HDR_MODE_OFF) {
+                    return;
+                } else {
+                    throw new UnsupportedOperationException(
+                            "Cannot set HDR on this device as requested.");
+                }
+            }
+
+            getHdrToggleButton().click();
+            // After clicking the HDR auto button should be visible.
+            mDevice.wait(Until.findObject(By.res(UI_PACKAGE_NAME, UI_HDR_AUTO_ID_4X)),
+                    DIALOG_TRANSITION_WAIT);
+
+            switch (mode) {
+                case HDR_MODE_AUTO:
+                    mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_HDR_AUTO_ID_4X)).click();
+                    break;
+                case HDR_MODE_ON:
+                    mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_HDR_ON_ID_4X)).click();
+                    break;
+                case HDR_MODE_OFF:
+                    mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_HDR_OFF_ID_4X)).click();
+                    break;
+                default:
+                    throw new UnknownUiException("Failing setting HDR+ mode!");
+            }
+            mDevice.waitForIdle();
+        } else if (mIsVersionI || mIsVersionJ) {
+            if (getHdrToggleButton() == null) {
+                if (mode == HDR_MODE_OFF) {
+                    return;
+                } else {
+                    throw new UnsupportedOperationException(
+                            "Cannot set HDR on this device as requested.");
+                }
+            }
+
+            for (int retries = 0; retries < 3; retries++) {
+                if (!isHdrMode(mode)) {
+                    getHdrToggleButton().click();
+                    mDevice.waitForIdle();
+                } else {
+                    Log.e(LOG_TAG, "Successfully set HDR mode!");
+                    mDevice.waitForIdle();
+                    return;
+                }
+            }
+        } else {
+            // Open mode options before checking Hdr status
+            openModeOptions2X();
+            if (getHdrToggleButton() == null) {
+                if (mode == HDR_MODE_OFF) {
+                    return;
+                } else {
+                    throw new UnsupportedOperationException(
+                            "Cannot set HDR on this device as requested.");
+                }
+            }
+
+            for (int retries = 0; retries < 3; retries++) {
+                if (!isHdrMode(mode)) {
+                    getHdrToggleButton().click();
+                    mDevice.waitForIdle();
+                } else {
+                    Log.e(LOG_TAG, "Successfully set HDR mode!");
+                    mDevice.waitForIdle();
+                    return;
+                }
+            }
+        }
+    }
+
+    private boolean isHdrMode(int mode) {
+        if (mIsVersionK) {
+            getHdrToggleButton().click();
+            mDevice.waitForIdle();
+            UiObject2 selectedOption = mDevice.wait(Until.findObject(
+                    By.res(UI_PACKAGE_NAME, UI_SELECTED_OPTION_ID)), MENU_WAIT_TIME);
+            String currentHdrModeText = selectedOption.getText();
+            int currentMode = 0;
+            switch (currentHdrModeText) {
+                case TEXT_HDR_AUTO:
+                    currentMode = HDR_MODE_AUTO;
+                    break;
+                case TEXT_HDR_ON:
+                    currentMode = HDR_MODE_ON;
+                    break;
+                case TEXT_HDR_OFF:
+                    currentMode = HDR_MODE_OFF;
+                    break;
+                default:
+                    throw new UnknownUiException("Failed to identify the HDR+ settings!");
+            }
+            selectedOption.click();
+            mDevice.wait(Until.findObject(
+                    By.res(UI_PACKAGE_NAME, UI_HDR_BUTTON_ID_4X)), MENU_WAIT_TIME);
+            return mode == currentMode;
+        } else if (mIsVersionI || mIsVersionJ) {
+            String modeDesc = getHdrToggleButton().getContentDescription();
+            if (DESC_HDR_AUTO.equals(modeDesc)) {
+                return HDR_MODE_AUTO == mode;
+            } else if (DESC_HDR_OFF_3X.equals(modeDesc)) {
+                return HDR_MODE_OFF == mode;
+            } else if (DESC_HDR_ON_3X.equals(modeDesc)) {
+                return HDR_MODE_ON == mode;
+            } else {
+                throw new UnknownUiException("Unexpected failure.");
+            }
+        } else {
+            // Open mode options before checking Hdr status
+            openModeOptions2X();
+            // Check the HDR mode
+            String modeDesc = getHdrToggleButton().getContentDescription();
+            if (DESC_HDR_OFF_2X.equals(modeDesc)) {
+                return HDR_MODE_OFF == mode;
+            } else if (DESC_HDR_ON_2X.equals(modeDesc)) {
+                return HDR_MODE_ON == mode;
+            } else {
+                throw new UnknownUiException("Unexpected failure.");
+            }
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void set4KMode(int mode) {
+        // If the menu is not open, open it
+        if (!isMenuOpen()) {
+            openMenu();
+        }
+
+        if (mIsVersionI || mIsVersionJ || mIsVersionK) {
+            // Select Menu Item "Settings"
+            selectMenuItem("Settings");
+        } else {
+            // Select Menu Item "Settings"
+            selectSetting2X();
+        }
+
+        if (mIsVersionI || mIsVersionJ) {
+            // Select Item "Resolution & Quality"
+            selectSettingItem("Resolution & quality");
+        }
+
+        // Select Item "Back camera video", which is the only mode supports 4k
+        selectVideoResolution(mode);
+
+        if (mIsVersionI || mIsVersionJ) {
+            // Quit Menu "Resolution & Quality"
+            closeSettingItem();
+        }
+
+        // Close Main Menu
+        closeMenuItem();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void setHFRMode(int mode) {
+        if (!isVideoMode()) {
+            throw new IllegalStateException("Must be in video mode to set HFR mode.");
+        }
+
+        // Haleakala doesn't support slow motion, so throw exception
+        if (mIsVersionH) {
+            throw new UnsupportedOperationException(
+                    "HFR not supported on this version of Google Camera.");
+        } else if (mIsVersionI) {
+            waitForHFRToggleEnabled();
+            for (int retries = 0; retries < 3; retries++) {
+                if (!isHfrMode(mode)) {
+                    getHfrToggleButton().click();
+                    mDevice.waitForIdle();
+                } else {
+                    Log.e(LOG_TAG, "Successfully set HFR mode!");
+                    mDevice.waitForIdle();
+                    waitForVideoShutterEnabled();
+                    return;
+                }
+            }
+            //If none of the 3 options match expected option, throw an exception
+            if (mode == HFR_MODE_OFF) {
+                throw new UnknownUiException("Failed to turn off the HFR mode");
+            } else {
+                throw new UnknownUiException(String.format("Failed to select HFR mode to FPS %d",
+                        (int) Math.floor(mode * 120)));
+            }
+        } else if (mIsVersionJ || mIsVersionK) {
+            String uiMenuButton = (mIsVersionK)? UI_MENU_BUTTON_ID_4X:UI_MENU_BUTTON_ID_3X;
+            if (mode == HFR_MODE_OFF) {
+                // This close button ui only appeared in hfr mode
+                UiObject2 hfrmodeclose = mDevice.findObject(By.res(UI_PACKAGE_NAME,
+                        UI_SPECIAL_MODE_CLOSE));
+                if (hfrmodeclose != null) {
+                    hfrmodeclose.click();
+                    mDevice.wait(Until.hasObject(By.res(UI_PACKAGE_NAME, uiMenuButton)),
+                            MENU_WAIT_TIME);
+                } else {
+                    throw new UnknownUiException(
+                            "Fail to find hfr mode close button when trying to turn off HFR mode");
+                }
+                return;
+            }
+
+            // When not in HFR interface, select menu to open HFR interface
+            if (mDevice.hasObject(By.res(UI_PACKAGE_NAME, UI_SPECIAL_MODE_CLOSE))
+                    && !isVideoMode()) {
+                UiObject2 specialmodeclose = mDevice.findObject(By.res(UI_PACKAGE_NAME,
+                        UI_SPECIAL_MODE_CLOSE));
+                if (specialmodeclose != null) {
+                    specialmodeclose.click();
+                    mDevice.wait(Until.hasObject(By.res(UI_PACKAGE_NAME, uiMenuButton)),
+                            MENU_WAIT_TIME);
+                } else {
+                    throw new UnknownUiException(
+                            "Fail to close other special mode before setting hfr mode");
+                }
+            }
+
+            if (!mDevice.hasObject(By.res(UI_PACKAGE_NAME, UI_SPECIAL_MODE_CLOSE))) {
+                // If the menu is not open, open it
+                if (!isMenuOpen()) {
+                    openMenu();
+                }
+                // Select Item "Slow Motion"
+                selectSettingItem("Slow Motion");
+                // Change Slow Motion mode to 120FPS or 240FPS
+            }
+
+            mDevice.waitForIdle();
+            // Detect if hfr toggle exists in the interface
+            if (!mDevice.hasObject(By.res(UI_PACKAGE_NAME, UI_HFR_TOGGLE_ID_J))) {
+                if (mode == HFR_MODE_240_FPS) {
+                    throw new UnknownUiException(
+                            "The 240 fps HFR mode is not supported on the device.");
+                }
+                return;
+            }
+
+            for (int retries = 0; retries < 2; retries++) {
+                if (!isHfrMode(mode)) {
+                    getHfrToggleButton().click();
+                    mDevice.waitForIdle();
+                } else {
+                    Log.e(LOG_TAG, "Successfully set HFR mode!");
+                    mDevice.waitForIdle();
+                    waitForVideoShutterEnabled();
+                    return;
+                }
+            }
+            //If neither of the 2 options match expected option, throw an exception
+            throw new UnknownUiException(String.format("Failed to select HFR mode to FPS %d",
+                    (int) Math.floor(mode * 120)));
+        } else {
+            throw new UnknownUiException("The Google Camera version is not supported.");
+        }
+    }
+
+    private boolean isHfrMode(int mode) {
+        if (mIsVersionI) {
+            String modeDesc = getHfrToggleButton().getContentDescription();
+            if (DESC_HFR_120_FPS.equals(modeDesc)) {
+                return HFR_MODE_120_FPS == mode;
+            } else if (DESC_HFR_240_FPS.equals(modeDesc)) {
+                return HFR_MODE_240_FPS == mode;
+            } else if (DESC_HFR_OFF.equals(modeDesc)) {
+                return HFR_MODE_OFF == mode;
+            } else {
+                throw new UnknownUiException("Fail to identify HFR toggle description.");
+            }
+        } else if (mIsVersionJ || mIsVersionK) {
+            if (getHfrToggleButton() == null) {
+                return HFR_MODE_OFF == mode;
+            }
+            String modeDesc = getHfrToggleButton().getContentDescription();
+            if (DESC_HFR_120_FPS.equals(modeDesc)) {
+                return HFR_MODE_120_FPS == mode;
+            } else if (DESC_HFR_240_FPS.equals(modeDesc)) {
+                return HFR_MODE_240_FPS == mode;
+            } else {
+                throw new UnknownUiException("Fail to identify HFR toggle description.");
+            }
+        }
+        return HFR_MODE_OFF == mode;
+    }
+
+    private void openModeOptions2X() {
+        // If the mode option is already open, return as it is
+        if (mDevice.hasObject(By.res(UI_PACKAGE_NAME, "mode_options_buttons"))) {
+            return;
+        }
+        // Before openning the mode option, close the menu if the menu is open
+        closeMenu();
+        waitForVideoShutterEnabled();
+        // Open the mode options to check HDR mode
+        UiObject2 modeoptions = getModeOptionsMenuButton();
+        if (modeoptions != null) {
+            modeoptions.click();
+            // If succeeded, the hdr toggle button should be visible.
+            mDevice.wait(Until.hasObject(By.res(UI_PACKAGE_NAME, "hdr_plus_toggle_button")),
+                    DIALOG_TRANSITION_WAIT);
+        } else {
+            throw new UnknownUiException(
+                    "Fail to find modeoption button when trying to check HDR mode");
+        }
+    }
+
+    private void openMenu() {
+        if (mIsVersionI || mIsVersionJ || mIsVersionK) {
+            String uiMenuButton = (mIsVersionK)? UI_MENU_BUTTON_ID_4X:UI_MENU_BUTTON_ID_3X;
+            UiObject2 menu = mDevice.findObject(By.res(UI_PACKAGE_NAME, uiMenuButton));
+            menu.click();
+        } else {
+            UiObject2 activityView = mDevice.wait(Until.findObject(
+                    By.res(UI_PACKAGE_NAME, UI_ACTIVITY_VIEW_ID)), MENU_WAIT_TIME);
+            activityView.swipe(Direction.RIGHT, 1.0f);
+        }
+
+        mDevice.wait(Until.hasObject(By.text("Photo Sphere")), MENU_WAIT_TIME);
+
+        mDevice.waitForIdle();
+    }
+
+    private void selectMenuItem(String mode) {
+        UiObject2 menuItem = mDevice.findObject(By.text(mode));
+        if (menuItem != null) {
+            menuItem.click();
+        } else {
+            throw new UnknownUiException(
+                    String.format("Menu item button was not enabled with %d seconds",
+                    (int)Math.floor(MENU_WAIT_TIME / 1000)));
+        }
+        mDevice.wait(Until.gone(By.text("Photo Sphere")), MENU_WAIT_TIME);
+
+        mDevice.waitForIdle();
+    }
+
+    private void closeMenuItem() {
+        UiObject2 navUp = mDevice.findObject(By.desc("Navigate up"));
+        if (navUp != null) {
+            navUp.click();
+        } else {
+            throw new UnknownUiException(String.format(
+                    "Navigation up button was not enabled with %d seconds",
+                    (int)Math.floor(MENU_WAIT_TIME / 1000)));
+        }
+        mDevice.wait(Until.gone(By.text("Help & feedback")), MENU_WAIT_TIME);
+
+        mDevice.waitForIdle();
+    }
+
+    private void selectSettingItem(String mode) {
+        UiObject2 settingItem = mDevice.findObject(By.text(mode));
+        if (settingItem != null) {
+            settingItem.click();
+        } else {
+            throw new UnknownUiException(
+                    String.format("Setting item button was not enabled with %d seconds",
+                    (int)Math.floor(MENU_WAIT_TIME / 1000)));
+        }
+        mDevice.wait(Until.gone(By.text("Help & feedback")), MENU_WAIT_TIME);
+
+        mDevice.waitForIdle();
+    }
+
+    private void selectSetting2X() {
+        UiObject2 settingItem = mDevice.findObject(By.desc("Settings"));
+        if (settingItem != null) {
+            settingItem.click();
+        } else {
+            throw new UnknownUiException(
+                    String.format("Setting item button was not enabled with %d seconds",
+                    (int)Math.floor(MENU_WAIT_TIME / 1000)));
+        }
+        mDevice.wait(Until.gone(By.text("Help & feedback")), MENU_WAIT_TIME);
+
+        mDevice.waitForIdle();
+    }
+
+    private void closeSettingItem() {
+        UiObject2 navUp = mDevice.findObject(By.desc("Navigate up"));
+        if (navUp != null) {
+            navUp.click();
+        } else {
+            throw new UnknownUiException(
+                    String.format("Navigation up button was not enabled with %d seconds",
+                    (int)Math.floor(MENU_WAIT_TIME / 1000)));
+        }
+        mDevice.wait(Until.findObject(By.text("Help & feedback")), MENU_WAIT_TIME);
+
+        mDevice.waitForIdle();
+    }
+
+    private void selectVideoResolution(int mode) {
+        String textBackVideoResolution =
+                (mIsVersionK)? TEXT_BACK_VIDEO_RESOLUTION_4X:TEXT_BACK_VIDEO_RESOLUTION_3X;
+        UiObject2 backCamera = mDevice.findObject(By.text(textBackVideoResolution));
+        if (backCamera != null) {
+            backCamera.click();
+        } else {
+            throw new UnknownUiException(
+                    String.format("Back camera button was not enabled with %d seconds",
+                    (int)Math.floor(MENU_WAIT_TIME / 1000)));
+        }
+        mDevice.wait(Until.findObject(By.text("CANCEL")), MENU_WAIT_TIME);
+        mDevice.waitForIdle();
+
+        if (mode == VIDEO_4K_MODE_ON) {
+            mDevice.wait(Until.findObject(By.text(TEXT_4K_ON)), MENU_WAIT_TIME).click();
+        } else if (mode == VIDEO_HD_1080) {
+            mDevice.wait(Until.findObject(By.text(TEXT_HD_1080)), MENU_WAIT_TIME).click();
+        } else if (mode == VIDEO_HD_720){
+            mDevice.wait(Until.findObject(By.text(TEXT_HD_720)), MENU_WAIT_TIME).click();
+        } else {
+            throw new UnknownUiException("Failed to set video resolution");
+        }
+
+        mDevice.wait(Until.gone(By.text("CANCEL")), MENU_WAIT_TIME);
+
+        mDevice.waitForIdle();
+    }
+
+    private void closeMenu() {
+        // Should only call this function when menu is open, do nothing if menu is not open
+        if (!isMenuOpen()) {
+            return;
+        }
+
+        if (mIsVersionI || mIsVersionJ || mIsVersionK) {
+            // Click menu button to close menu (this is NOT for taking pictures)
+            String uiMenuButton = (mIsVersionK)? UI_MENU_BUTTON_ID_4X:UI_MENU_BUTTON_ID_3X;
+            UiObject2 backButton = mDevice.findObject(By.res(UI_PACKAGE_NAME, uiMenuButton));
+            if (backButton != null) {
+                backButton.click();
+            }
+        } else {
+            // Click shutter button to close menu (this is NOT for taking pictures)
+            UiObject2 shutter = mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_SHUTTER_BUTTON_ID_2X));
+            if (shutter != null) {
+                shutter.click();
+            }
+        }
+    }
+
+    private boolean isCameraMode() {
+        if (mIsVersionI || mIsVersionJ || mIsVersionK) {
+            return (mDevice.hasObject(By.desc(UI_SHUTTER_DESC_CAM_3X)));
+        } else {
+            // TODO: identify a Haleakala UiObject2 unique Camera mode
+            return !isVideoMode();
+        }
+    }
+
+    private boolean isVideoMode() {
+        if (mIsVersionI || mIsVersionJ || mIsVersionK) {
+            return (mDevice.hasObject(By.desc(UI_SHUTTER_DESC_VID_3X)));
+        } else {
+            return (mDevice.hasObject(By.res(UI_PACKAGE_NAME, "recording_time_rect")));
+        }
+    }
+
+    private boolean isRecording() {
+        return mDevice.hasObject(By.res(UI_PACKAGE_NAME, UI_RECORDING_TIME_ID));
+    }
+
+    private boolean isFrontCamera() {
+        // Close menu if open
+        closeMenu();
+
+        if (mIsVersionJ || mIsVersionK) {
+            return (mDevice.hasObject(By.desc("Switch to back camera")));
+        } else if (mIsVersionI) {
+            return (mDevice.hasObject(By.desc("Front camera")));
+        } else {
+            // Open mode options if not open
+            UiObject2 modeoptions = getModeOptionsMenuButton();
+            if (modeoptions != null) {
+                modeoptions.click();
+            }
+            return (mDevice.hasObject(By.desc("Front camera")));
+        }
+    }
+
+    private boolean isBackCamera() {
+        // Close menu if open
+        closeMenu();
+
+        if (mIsVersionJ || mIsVersionK) {
+            return (mDevice.hasObject(By.desc("Switch to front camera")));
+        } else if (mIsVersionI) {
+            return (mDevice.hasObject(By.desc("Back camera")));
+        } else {
+            // Open mode options if not open
+            UiObject2 modeoptions = getModeOptionsMenuButton();
+            if (modeoptions != null) {
+                modeoptions.click();
+            }
+            return (mDevice.hasObject(By.desc("Back camera")));
+        }
+    }
+
+    private boolean isMenuOpen() {
+        if (mIsVersionI || mIsVersionJ || mIsVersionK) {
+            if (mDevice.hasObject(By.desc("Open settings"))) {
+                return true;
+            }
+        } else {
+            if (mDevice.hasObject(By.res(UI_PACKAGE_NAME, UI_SETTINGS_BUTTON_ID))) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private void pressBackFrontToggleButton() {
+        UiObject2 toggle = getBackFrontToggleButton();
+        if (toggle != null) {
+            toggle.click();
+        } else {
+            throw new UnknownUiException("Failed to detect a back-front toggle button");
+        }
+    }
+
+    private UiObject2 getCameraVideoToggleButton() {
+        return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_TOGGLE_BUTTON_ID));
+    }
+
+    private UiObject2 getBackFrontToggleButton() {
+        return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_BACK_FRONT_TOGGLE_BUTTON_ID));
+    }
+
+    private UiObject2 getHdrToggleButton() {
+        if (mIsVersionK) {
+            return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_HDR_BUTTON_ID_4X));
+        } else if (mIsVersionI || mIsVersionJ) {
+            return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_HDR_BUTTON_ID_3X));
+        } else {
+            return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_HDR_BUTTON_ID_2X));
+        }
+    }
+
+    private UiObject2 getHfrToggleButton() {
+        if (mIsVersionI) {
+            return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_HFR_TOGGLE_ID_I));
+        } else if (mIsVersionJ || mIsVersionK) {
+            return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_HFR_TOGGLE_ID_J));
+        } else {
+            throw new UnsupportedOperationException(
+                    "HFR not supported on this version of Google Camera.");
+        }
+    }
+
+    private UiObject2 getModeOptionsMenuButton() {
+        return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_MODE_OPTION_TOGGLE_BUTTON_ID));
+    }
+
+    private UiObject2 getCameraShutter() {
+        if (mIsVersionI || mIsVersionJ || mIsVersionK) {
+            return mDevice.findObject(By.desc(UI_SHUTTER_DESC_CAM_3X).enabled(true));
+        } else {
+            return mDevice.findObject(By.desc(UI_SHUTTER_DESC_CAM_2X).enabled(true));
+        }
+    }
+
+    private UiObject2 getVideoShutter() {
+        if (mIsVersionI || mIsVersionJ || mIsVersionK) {
+            return mDevice.findObject(By.desc(UI_SHUTTER_DESC_VID_3X).enabled(true));
+        } else {
+            return mDevice.findObject(By.desc(UI_SHUTTER_DESC_VID_2X).enabled(true));
+        }
+    }
+
+    private UiObject2 getThumbnailAlbumButton() {
+        return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_THUMBNAIL_ALBUM_BUTTON_ID));
+    }
+
+    private UiObject2 getAlbumFilmstripView() {
+        return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_ALBUM_FILMSTRIP_VIEW_ID));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void waitForCameraShutterEnabled() {
+        boolean uiSuccess = false;
+
+        if (mIsVersionI || mIsVersionJ || mIsVersionK) {
+            uiSuccess = mDevice.wait(Until.hasObject(
+                    By.desc(UI_SHUTTER_DESC_CAM_3X).enabled(true)), SHUTTER_WAIT_TIME);
+        } else {
+            uiSuccess = mDevice.wait(Until.hasObject(
+                    By.desc(UI_SHUTTER_DESC_CAM_2X).enabled(true)), SHUTTER_WAIT_TIME);
+        }
+
+        if (!uiSuccess) {
+            throw new UnknownUiException(
+                    String.format("Camera shutter was not enabled with %d seconds",
+                    (int)Math.floor(SHUTTER_WAIT_TIME / 1000)));
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void waitForVideoShutterEnabled() {
+        boolean uiSuccess = false;
+
+        if (mIsVersionI || mIsVersionJ || mIsVersionK) {
+            uiSuccess = mDevice.wait(Until.hasObject(
+                    By.desc(UI_SHUTTER_DESC_VID_3X).enabled(true)), SHUTTER_WAIT_TIME);
+        } else {
+            uiSuccess = mDevice.wait(Until.hasObject(
+                    By.desc(UI_SHUTTER_DESC_VID_2X).enabled(true)), SHUTTER_WAIT_TIME);
+        }
+
+        if (!uiSuccess) {
+            throw new UnknownUiException(
+                    String.format("Video shutter was not enabled with %d seconds",
+                    (int)Math.floor(SHUTTER_WAIT_TIME / 1000)));
+        }
+    }
+
+    private void waitForCurrentShutterEnabled() {
+        // This function is called to wait for shutter button enabled in either camera or video mode
+        if (mIsVersionI || mIsVersionJ || mIsVersionK) {
+            mDevice.wait(Until.hasObject(By.res(UI_PACKAGE_NAME, UI_SHUTTER_BUTTON_ID_3X).enabled(true)),
+                    SHUTTER_WAIT_TIME);
+        } else {
+            mDevice.wait(Until.hasObject(By.res(UI_PACKAGE_NAME, UI_SHUTTER_BUTTON_ID_2X).enabled(true)),
+                    SHUTTER_WAIT_TIME);
+        }
+    }
+
+    private void waitForBackEnabled() {
+        if (mIsVersionI || mIsVersionJ || mIsVersionK) {
+            mDevice.wait(Until.hasObject(By.desc("Switch to front camera").enabled(true)),
+                    SWITCH_WAIT_TIME);
+        } else {
+            mDevice.wait(Until.hasObject(By.desc("Back camera").enabled(true)),
+                    SWITCH_WAIT_TIME);
+        }
+    }
+
+    private void waitForFrontEnabled() {
+        if (mIsVersionI || mIsVersionJ || mIsVersionK) {
+            mDevice.wait(Until.hasObject(By.desc("Switch to back camera").enabled(true)),
+                    SWITCH_WAIT_TIME);
+        } else {
+            mDevice.wait(Until.hasObject(By.desc("Front camera").enabled(true)),
+                    SWITCH_WAIT_TIME);
+        }
+    }
+
+    private void waitForHFRToggleEnabled() {
+        if (mIsVersionJ || mIsVersionK) {
+            mDevice.wait(Until.hasObject(By.res(UI_PACKAGE_NAME, UI_HFR_TOGGLE_ID_J).enabled(true)),
+                    SWITCH_WAIT_TIME);
+        } else if (mIsVersionI) {
+            mDevice.wait(Until.hasObject(By.res(UI_PACKAGE_NAME, UI_HFR_TOGGLE_ID_I).enabled(true)),
+                    SWITCH_WAIT_TIME);
+        } else {
+            throw new UnknownUiException("HFR is not supported on this version of Google Camera");
+        }
+    }
+
+    private void waitForAppInit() {
+        boolean initalized = false;
+        if (mIsVersionI || mIsVersionJ || mIsVersionK) {
+            String uiMenuButton = (mIsVersionK)? UI_MENU_BUTTON_ID_4X:UI_MENU_BUTTON_ID_3X;
+            initalized = mDevice.wait(Until.hasObject(By.res(UI_PACKAGE_NAME, uiMenuButton)),
+                    APP_INIT_WAIT);
+        } else {
+            initalized = mDevice.wait(Until.hasObject(By.res(UI_PACKAGE_NAME, UI_MODE_OPTION_TOGGLE_BUTTON_ID)),
+                    APP_INIT_WAIT);
+        }
+
+        waitForCurrentShutterEnabled();
+
+        mDevice.waitForIdle();
+
+        if (initalized) {
+            Log.e(LOG_TAG, "Successfully initialized.");
+        } else {
+            Log.e(LOG_TAG, "Failed to find initialization indicator.");
+        }
+    }
+
+    /**
+     * TODO: Temporary. Create long-term solution for registering watchers.
+     */
+    public void registerCrashWatcher() {
+        final UiDevice fDevice = mDevice;
+
+        mDevice.registerWatcher("GoogleCamera-crash-watcher", new UiWatcher() {
+            @Override
+            public boolean checkForCondition() {
+                Pattern dismissWords =
+                        Pattern.compile("DISMISS", Pattern.CASE_INSENSITIVE);
+                UiObject2 buttonDismiss = fDevice.findObject(By.text(dismissWords).enabled(true));
+                if (buttonDismiss != null) {
+                    buttonDismiss.click();
+                    throw new UnknownUiException("Camera crash dialog encountered. Failing test.");
+                }
+
+                return false;
+            }
+        });
+    }
+
+    /**
+     * TODO: Temporary. Create long-term solution for registering watchers.
+     */
+    public void unregisterCrashWatcher() {
+        mDevice.removeWatcher("GoogleCamera-crash-watcher");
+    }
+
+    /**
+     * TODO: Should only be temporary
+     * {@inheritDoc}
+     */
+    public String openWithShutterTimeString() {
+        String pkg = getPackage();
+        String id = getLauncherName();
+
+        long launchStart = ILauncherStrategy.LAUNCH_FAILED_TIMESTAMP;
+        if (!mDevice.hasObject(By.pkg(pkg).depth(0))) {
+            launchStart = mLauncherStrategy.launch(id, pkg);
+        }
+
+        if (launchStart == ILauncherStrategy.LAUNCH_FAILED_TIMESTAMP) {
+            throw new UnknownUiException("Failed to launch GoogleCamera.");
+        }
+
+        waitForAppInit();
+        waitForCurrentShutterEnabled();
+        long launchDuration = SystemClock.uptimeMillis() - launchStart;
+
+        Date dateNow = new Date();
+        DateFormat dateFormat = new SimpleDateFormat("MM-dd HH:mm:ss.SSS");
+        String dateString = dateFormat.format(dateNow);
+
+        if (isCameraMode()) {
+            return String.format("%s %s %d\n", dateString, "camera", launchDuration);
+        } else if (isVideoMode()) {
+            return String.format("%s %s %d\n", dateString, "video", launchDuration);
+        } else {
+            return String.format("%s %s %d\n", dateString, "wtf", launchDuration);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void goToAlbum() {
+        UiObject2 thumbnailAlbumButton = getThumbnailAlbumButton();
+        if (thumbnailAlbumButton == null) {
+            throw new UnknownUiException("Could not find thumbnail album button");
+        }
+
+        thumbnailAlbumButton.click();
+        if (!mDevice.wait(Until.hasObject(
+                By.res(UI_PACKAGE_NAME, UI_ALBUM_FILMSTRIP_VIEW_ID)), DIALOG_TRANSITION_WAIT)) {
+            throw new UnknownUiException("Could not find album filmstrip");
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void scrollAlbum(Direction direction) {
+        if (!(Direction.LEFT.equals(direction) || Direction.RIGHT.equals(direction))) {
+            throw new IllegalArgumentException("direction must be LEFT or RIGHT");
+        }
+
+        UiObject2 albumFilmstripView = getAlbumFilmstripView();
+        if (albumFilmstripView == null) {
+            throw new UnknownUiException("Could not find album filmstrip view");
+        }
+
+        albumFilmstripView.scroll(direction, 5.0f);
+        mDevice.waitForIdle();
+    }
+}
diff --git a/libraries/google-docs-app-helper/Android.mk b/libraries/google-docs-app-helper/Android.mk
new file mode 100644
index 0000000..44ee3c3
--- /dev/null
+++ b/libraries/google-docs-app-helper/Android.mk
@@ -0,0 +1,23 @@
+#
+# 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.
+#
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := google-docs-app-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator base-app-helpers
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/google-docs-app-helper/src/android/platform/test/helpers/GoogleDocsHelperImpl.java b/libraries/google-docs-app-helper/src/android/platform/test/helpers/GoogleDocsHelperImpl.java
new file mode 100644
index 0000000..d9a31f8
--- /dev/null
+++ b/libraries/google-docs-app-helper/src/android/platform/test/helpers/GoogleDocsHelperImpl.java
@@ -0,0 +1,178 @@
+/*
+ * 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.os.SystemClock;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.Until;
+import android.support.test.uiautomator.UiObject2;
+
+import junit.framework.Assert;
+
+import java.lang.IllegalArgumentException;
+import java.lang.IllegalStateException;
+
+/**
+ * UI test helper for Google Docs (package: com.google.android.apps.docs.editors.docs).
+ * Implementation based on app version: 1.6.152
+ */
+
+public class GoogleDocsHelperImpl extends AbstractGoogleDocsHelper {
+
+    private static final String LOG_TAG = GoogleDocsHelperImpl.class.getSimpleName();
+
+    private static final String UI_PACKAGE_NAME = "com.google.android.apps.docs.editors.docs";
+    private static final String UI_DOCS_LIST_TITLE = "title";
+    private static final String UI_DOCS_LIST_VIEW = "doc_list_view";
+    private static final String UI_EDITOR_VIEW = "kix_editor_view";
+    private static final String UI_TOOLBAR = "toolbar";
+    private static final String UI_TEXT_DOCS = "Docs";
+    private static final String UI_TEXT_SKIP = "SKIP";
+
+    private static final long HACKY_DELAY = 1000; // 1 sec
+    private static final long LOAD_DOCUMENT_TIMEOUT = 60000; // 60 secs
+    private static final int BACK_TO_RECENT_DOCS_MAX_RETRY = 5;
+    private static final int SEARCHING_DOC_MAX_SCROLL_DOWN = 10;
+
+    public GoogleDocsHelperImpl(Instrumentation instr) {
+        super(instr);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getPackage() {
+        return "com.google.android.apps.docs.editors.docs";
+    }
+
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getLauncherName() {
+        return "Docs";
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void dismissInitialDialogs() {
+        UiObject2 skipButton = getSkipButton();
+        if (skipButton != null) {
+            skipButton.click();
+            mDevice.waitForIdle();
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void goToRecentDocsTab() {
+        for (int retryCnt = 0; retryCnt < BACK_TO_RECENT_DOCS_MAX_RETRY; retryCnt++) {
+            if (isOnRecentDocsTab()) {
+                return;
+            }
+            mDevice.pressBack();
+
+            // TODO Hacky workaround
+            // Bug: 28675538
+            // mDevice.waitForIdle() is insufficient when a short (unscrollable)
+            // document is scrolled by scrollDownDocument() before goToRecentDocsTab()
+            // is called. isOnRecentDocsTab() fails to recognize the Recent Docs tab
+            // even if the tab is indeed shown.
+            SystemClock.sleep(HACKY_DELAY);
+        }
+        Assert.assertTrue("Failed to go to Recent Docs Tab", isOnRecentDocsTab());
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void openDoc(String title) {
+        if (!isOnRecentDocsTab()) {
+            throw new IllegalStateException("Not on the Recent Docs tab");
+        }
+        UiObject2 recentDocsList = getRecentDocsList();
+
+        // TODO: Hacky workaround
+        // Bug: 28675621
+        // while (recentDocsList.scroll(Direction.UP, 1.0f));
+        // The above while loop doesn't work as scroll doesn't return true
+        // while there's more to scroll.
+        recentDocsList.fling(Direction.UP);
+
+        for (int cnt = 0; cnt < SEARCHING_DOC_MAX_SCROLL_DOWN; cnt++) {
+            UiObject2 documentTitle = recentDocsList.findObject(
+                    By.res(UI_PACKAGE_NAME, UI_DOCS_LIST_TITLE).text(title));
+            if (documentTitle != null) {
+                // document is found, click to download
+                documentTitle.click();
+                boolean editorLoaded = mDevice.wait(
+                        Until.hasObject(By.res(UI_PACKAGE_NAME, UI_EDITOR_VIEW)),
+                        LOAD_DOCUMENT_TIMEOUT);
+                Assert.assertTrue(String.format("Failed to finish downloading %s", title),
+                        editorLoaded);
+                return;
+            }
+            recentDocsList.scroll(Direction.DOWN, 0.5f);
+        }
+        Assert.fail(String.format("Can't find the document: %s", title));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void scrollDownDocument() {
+        UiObject2 docsEditorPage = getDocsEditorPage();
+        if (docsEditorPage == null) {
+            throw new IllegalStateException("Not on a document page");
+        }
+        docsEditorPage.scroll(Direction.DOWN, 1.0f);
+    }
+
+    private boolean isOnRecentDocsTab() {
+        UiObject2 toolbar = getToolbar();
+        if (toolbar == null) {
+            return false;
+        }
+        return toolbar.hasObject(By.text(UI_TEXT_DOCS));
+    }
+
+    private UiObject2 getToolbar() {
+        return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_TOOLBAR));
+    }
+
+    private UiObject2 getRecentDocsList() {
+        return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_DOCS_LIST_VIEW));
+    }
+
+    private UiObject2 getDocsEditorPage() {
+        return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_EDITOR_VIEW));
+    }
+
+    private UiObject2 getSkipButton() {
+        return mDevice.findObject(By.text(UI_TEXT_SKIP));
+    }
+}
diff --git a/libraries/google-keyboard-app-helper/Android.mk b/libraries/google-keyboard-app-helper/Android.mk
new file mode 100644
index 0000000..26d952c
--- /dev/null
+++ b/libraries/google-keyboard-app-helper/Android.mk
@@ -0,0 +1,23 @@
+#
+# 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.
+#
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := google-keyboard-app-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator base-app-helpers
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/google-keyboard-app-helper/src/android/platform/test/helpers/GoogleKeyboardHelperImpl.java b/libraries/google-keyboard-app-helper/src/android/platform/test/helpers/GoogleKeyboardHelperImpl.java
new file mode 100644
index 0000000..cb43c99
--- /dev/null
+++ b/libraries/google-keyboard-app-helper/src/android/platform/test/helpers/GoogleKeyboardHelperImpl.java
@@ -0,0 +1,335 @@
+/*
+ * 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.pm.PackageManager.NameNotFoundException;
+import android.os.SystemClock;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Until;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.util.Log;
+
+import junit.framework.Assert;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.regex.Pattern;
+import java.util.Set;
+
+public class GoogleKeyboardHelperImpl extends AbstractGoogleKeyboardHelper {
+    private static final String TAG = GoogleKeyboardHelperImpl.class.getCanonicalName();
+
+    private static final Map<Character, String> SPECIAL_KEY_CONTENT_DESCRIPTIONS;
+
+    private static final Set<Character> ALWAYS_VISIBLE_CHARACTERS;
+    private static final Set<Character> KEYBOARD_NUMBER_SCREEN_SYMBOLS;
+    private static final Set<Character> KEYBOARD_OTHER_SYMBOLS;
+
+    private static final String UI_ANDROID_VIEW_CLASS = "android.view.View";
+    private static final String UI_DECLINE_BUTTON_ID = "decline_button";
+    private static final String UI_KEYBOARD_KEY_CLASS = "com.android.inputmethod.keyboard.Key";
+    private static final String UI_KEYBOARD_LETTER_KEY_DESC = "Letters";
+    private static final String UI_KEYBOARD_NUMBER_KEY_DESC = "Symbols";
+    private static final String UI_KEYBOARD_SHIFT_KEY_DESC = "Shift";
+    private static final String UI_KEYBOARD_SYMBOL_KEY_DESC = "More symbols";
+    private static final String UI_KEYBOARD_VIEW_ID = "keyboard_view";
+    private static final String UI_RESOURCE_NAME = "com.android.inputmethod.latin";
+    private static final String UI_PACKAGE_NAME = "com.google.android.inputmethod.latin";
+    private static final String UI_QUICK_SEARCH_BOX_PACKAGE_NAME =
+            "com.google.android.googlequicksearchbox";
+
+    private static final char KEYBOARD_TEST_LOWER_CASE_LETTER = 'a';
+    private static final char KEYBOARD_TEST_NUMBER = '1';
+    private static final char KEYBOARD_TEST_SYMBOL = '~';
+    private static final char KEYBOARD_TEST_UPPER_CASE_LETTER = 'A';
+
+    private static final long KEYBOARD_MODE_CHANGE_TIMEOUT = 5000; // 5 secs
+
+    static {
+        Map<Character, String> specialKeyContentDescriptions = new HashMap<>();
+        specialKeyContentDescriptions.put(' ', "Space");
+        specialKeyContentDescriptions.put('I', "Capital I");
+        specialKeyContentDescriptions.put('Δ', "Increment");
+        specialKeyContentDescriptions.put('©', "Copyright sign");
+        specialKeyContentDescriptions.put('®', "Registered sign");
+        specialKeyContentDescriptions.put('™', "Trade mark sign");
+        specialKeyContentDescriptions.put('â„…', "Care of");
+        SPECIAL_KEY_CONTENT_DESCRIPTIONS =
+                Collections.unmodifiableMap(specialKeyContentDescriptions);
+
+        String alwaysVisibleCharacters = ".,";
+        ALWAYS_VISIBLE_CHARACTERS = createImmutableSet(alwaysVisibleCharacters);
+
+        String keyboardNumberScreenSymbols = "@#$%&-+()*\"':;!?_/";
+        KEYBOARD_NUMBER_SCREEN_SYMBOLS = createImmutableSet(keyboardNumberScreenSymbols);
+
+        String keyboardOtherSymbols = "~`|•√π÷×¶Δ£¢€¥^°={}\\©®™â„…[]<>";
+        KEYBOARD_OTHER_SYMBOLS = createImmutableSet(keyboardOtherSymbols);
+    }
+
+    public GoogleKeyboardHelperImpl(Instrumentation instr) {
+        super(instr);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void open() {
+        Log.w(TAG, "No method defined to open Google Keyboard. (no-op)");
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getPackage() {
+        return UI_PACKAGE_NAME;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getLauncherName() {
+        return "Google Keyboard";
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void dismissInitialDialogs() {
+        mDevice.pressHome();
+        UiObject2 searchPlate = mDevice.findObject(
+                By.res(UI_QUICK_SEARCH_BOX_PACKAGE_NAME, "search_plate"));
+        if (searchPlate != null) {
+            searchPlate.click();
+            UiObject2 skipGoogleNowButton = mDevice.wait(Until.findObject(
+                    By.res(UI_QUICK_SEARCH_BOX_PACKAGE_NAME, UI_DECLINE_BUTTON_ID)), 20000);
+            if (skipGoogleNowButton != null) {
+                skipGoogleNowButton.click();
+            }
+            BySelector closeSelector = By.text(Pattern.compile("CLOSE", Pattern.CASE_INSENSITIVE));
+            Assert.assertTrue("Could not find close button to dismiss Google Keyboard dialog",
+                    mDevice.wait(Until.hasObject(closeSelector), 5000));
+            mDevice.findObject(closeSelector).click();
+            mDevice.wait(Until.gone(closeSelector), 5000);
+        }
+
+    }
+
+    private static Set<Character> createImmutableSet(String setCharacters) {
+        Assert.assertNotNull("setCharacters cannot be null", setCharacters);
+
+        Set<Character> tempSet = new HashSet<>();
+        for (int i = 0; i < setCharacters.length(); ++i) {
+            tempSet.add(setCharacters.charAt(i));
+        }
+        return Collections.unmodifiableSet(tempSet);
+    }
+
+    private UiObject2 getKeyboardView() {
+        return mDevice.findObject(By.clazz(UI_ANDROID_VIEW_CLASS).res(
+                UI_RESOURCE_NAME, UI_KEYBOARD_VIEW_ID));
+    }
+
+    private UiObject2 getShiftKey() {
+        return mDevice.findObject(
+                By.clazz(UI_KEYBOARD_KEY_CLASS).desc(UI_KEYBOARD_SHIFT_KEY_DESC));
+    }
+
+    private UiObject2 getNumberKey() {
+        return mDevice.findObject(
+                By.clazz(UI_KEYBOARD_KEY_CLASS).desc(UI_KEYBOARD_NUMBER_KEY_DESC));
+    }
+
+    private UiObject2 getLetterKey() {
+        return mDevice.findObject(
+                By.clazz(UI_KEYBOARD_KEY_CLASS).desc(UI_KEYBOARD_LETTER_KEY_DESC));
+    }
+
+    private UiObject2 getSymbolKey() {
+        return mDevice.findObject(
+                By.clazz(UI_KEYBOARD_KEY_CLASS).desc(UI_KEYBOARD_SYMBOL_KEY_DESC));
+    }
+
+    private String getKeyDesc(char key) {
+        String specialKeyDesc = SPECIAL_KEY_CONTENT_DESCRIPTIONS.get(key);
+        if (specialKeyDesc != null) {
+            return specialKeyDesc;
+        } else {
+            return String.valueOf(key);
+        }
+    }
+
+    private UiObject2 getKeyboardKey(char key) {
+        String keyDesc = getKeyDesc(key);
+
+        return mDevice.findObject(By.clazz(UI_KEYBOARD_KEY_CLASS).desc(keyDesc));
+    }
+
+    private boolean isLowerCaseLetter(char c) {
+        return (c >= 'a' && c <= 'z');
+    }
+
+    private boolean isUpperCaseLetter(char c) {
+        return (c >= 'A' && c <= 'Z');
+    }
+
+    private boolean isDigit(char c) {
+        return (c >= '0' && c <= '9');
+    }
+
+    private boolean isKeyboardOpen() {
+        return (getKeyboardView() != null);
+    }
+
+    private boolean isOnLowerCaseMode() {
+        return (getKeyboardKey(KEYBOARD_TEST_LOWER_CASE_LETTER) != null);
+    }
+
+    private boolean isOnUpperCaseMode() {
+        return (getKeyboardKey(KEYBOARD_TEST_UPPER_CASE_LETTER) != null);
+    }
+
+    private boolean isOnNumberMode() {
+        return (getKeyboardKey(KEYBOARD_TEST_NUMBER) != null);
+    }
+
+    private boolean isOnSymbolMode() {
+        return (getKeyboardKey(KEYBOARD_TEST_SYMBOL) != null);
+    }
+
+    private void toggleShiftMode() {
+        UiObject2 shiftKey = getShiftKey();
+        Assert.assertNotNull("Could not find Shift key", shiftKey);
+
+        shiftKey.click();
+    }
+
+    private void switchToLetterMode() {
+        UiObject2 letterKey = getLetterKey();
+        Assert.assertNotNull("Could not find Letter key", letterKey);
+
+        letterKey.click();
+    }
+
+    private void switchToLowerCaseMode() {
+        if (isOnNumberMode() || isOnSymbolMode()) {
+            switchToLetterMode();
+        }
+
+        if (isOnUpperCaseMode()) {
+            toggleShiftMode();
+        }
+
+        Assert.assertTrue("Could not switch to lower case letters mode on Google Keyboard",
+                mDevice.wait(Until.hasObject(By.clazz(UI_KEYBOARD_KEY_CLASS).desc(
+                String.valueOf(KEYBOARD_TEST_LOWER_CASE_LETTER))), KEYBOARD_MODE_CHANGE_TIMEOUT));
+    }
+
+    private void switchToUpperCaseMode() {
+        if (isOnNumberMode() || isOnSymbolMode()) {
+            switchToLetterMode();
+        }
+
+        if (isOnLowerCaseMode()) {
+            toggleShiftMode();
+        }
+
+        Assert.assertTrue("Could not switch to upper case letters mode on Google Keyboard",
+                mDevice.wait(Until.hasObject(By.clazz(UI_KEYBOARD_KEY_CLASS).desc(
+                String.valueOf(KEYBOARD_TEST_UPPER_CASE_LETTER))), KEYBOARD_MODE_CHANGE_TIMEOUT));
+    }
+
+    private void switchToNumberMode() {
+        if (!isOnNumberMode()) {
+            UiObject2 numberKey = getNumberKey();
+            Assert.assertNotNull("Could not find Number key", numberKey);
+
+            numberKey.click();
+        }
+
+        Assert.assertTrue("Could not switch to number mode on Google Keyboard",
+                mDevice.wait(Until.hasObject(By.clazz(UI_KEYBOARD_KEY_CLASS).desc(
+                String.valueOf(KEYBOARD_TEST_NUMBER))), KEYBOARD_MODE_CHANGE_TIMEOUT));
+    }
+
+    private void switchToSymbolMode() {
+        if (isOnLowerCaseMode() || isOnUpperCaseMode()) {
+            switchToNumberMode();
+        }
+
+        if (isOnNumberMode()) {
+            UiObject2 symbolKey = getSymbolKey();
+            Assert.assertNotNull("Could not find Symbol key", symbolKey);
+
+            symbolKey.click();
+        }
+
+        Assert.assertTrue("Could not switch to symbol mode on Google Keyboard",
+                mDevice.wait(Until.hasObject(By.clazz(UI_KEYBOARD_KEY_CLASS).desc(
+                String.valueOf(KEYBOARD_TEST_SYMBOL))), KEYBOARD_MODE_CHANGE_TIMEOUT));
+    }
+
+    /*
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean waitForKeyboard(long timeout) {
+        return mDevice.wait(Until.hasObject(By.clazz(UI_ANDROID_VIEW_CLASS).res(
+                UI_RESOURCE_NAME, UI_KEYBOARD_VIEW_ID)), timeout);
+    }
+
+    /*
+     * {@inheritDoc}
+     */
+    @Override
+    public void typeText(String text, long delayBetweenKeyPresses) {
+        Assert.assertTrue("Google Keyboard is not open", isKeyboardOpen());
+        for (int i = 0; i < text.length(); ++i) {
+            char c = text.charAt(i);
+
+            if (ALWAYS_VISIBLE_CHARACTERS.contains(c)) {
+                // Period and comma are visible on all keyboard modes so no need to switch modes
+            } else if (isLowerCaseLetter(c)) {
+                switchToLowerCaseMode();
+            } else if (isUpperCaseLetter(c)) {
+                switchToUpperCaseMode();
+            } else if (isDigit(c) ||
+                    KEYBOARD_NUMBER_SCREEN_SYMBOLS.contains(c)) {
+                switchToNumberMode();
+            } else if (KEYBOARD_OTHER_SYMBOLS.contains(c)) {
+                switchToSymbolMode();
+            } else {
+                Assert.fail(String.format("Unrecognized character '%c'", c));
+            }
+            UiObject2 keyboardKey = getKeyboardKey(c);
+            Assert.assertNotNull(String.format("Could not find key '%c' on Google Keyboard", c),
+                    keyboardKey);
+
+            keyboardKey.click();
+            SystemClock.sleep(delayBetweenKeyPresses);
+        }
+    }
+}
diff --git a/libraries/google-messenger-app-helper/Android.mk b/libraries/google-messenger-app-helper/Android.mk
new file mode 100644
index 0000000..cf548b0
--- /dev/null
+++ b/libraries/google-messenger-app-helper/Android.mk
@@ -0,0 +1,23 @@
+#
+# 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.
+#
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := google-messenger-app-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator base-app-helpers
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/google-messenger-app-helper/src/android/platform/test/helpers/GoogleMessengerHelperImpl.java b/libraries/google-messenger-app-helper/src/android/platform/test/helpers/GoogleMessengerHelperImpl.java
new file mode 100644
index 0000000..d7b08c0
--- /dev/null
+++ b/libraries/google-messenger-app-helper/src/android/platform/test/helpers/GoogleMessengerHelperImpl.java
@@ -0,0 +1,275 @@
+/*
+ * 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.pm.PackageManager.NameNotFoundException;
+import android.os.SystemClock;
+import android.platform.test.helpers.exceptions.UnknownUiException;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.Until;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.util.Log;
+
+import java.util.List;
+
+public class GoogleMessengerHelperImpl extends AbstractGoogleMessengerHelper {
+    private static final String TAG = GoogleMessengerHelperImpl.class.getSimpleName();
+
+    private static final String UI_ATTACH_MEDIA_BUTTON_ID = "attach_media_button";
+    private static final String UI_CHOOSE_PHOTO_TEXT = "Choose photo";
+    private static final String UI_COMPOSE_MESSAGE_TEXT_ID = "compose_message_text";
+    private static final String UI_CONTACT_NAME_ID = "contact_name";
+    private static final String UI_MEDIA_FROM_DEVICE_DESC = "Choose images from this device";
+    private static final String UI_MEDIA_GALLERY_GRID_VIEW_ID = "gallery_grid_view";
+    private static final String UI_MEDIA_PICKER_TABSTRIP_ID = "mediapicker_tabstrip";
+    private static final String UI_PACKAGE_NAME = "com.google.android.apps.messaging";
+    private static final String UI_RECIPIENT_TEXT_VIEW_ID = "recipient_text_view";
+    private static final String UI_SEND_MESSAGE_BUTTON_ID = "send_message_button";
+    private static final String UI_START_NEW_CONVERSATION_BUTTON_ID =
+            "start_new_conversation_button";
+
+    private static final long UI_DIALOG_WAIT = 5000; // 5 sec
+
+    public GoogleMessengerHelperImpl(Instrumentation instr) {
+        super(instr);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getPackage() {
+        return UI_PACKAGE_NAME;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getLauncherName() {
+        return "Messenger";
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void dismissInitialDialogs() {
+
+    }
+
+    private UiObject2 getStartNewConversationButton() {
+        return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_START_NEW_CONVERSATION_BUTTON_ID));
+    }
+
+    private UiObject2 getRecipientTextView() {
+        return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_RECIPIENT_TEXT_VIEW_ID));
+    }
+
+    private UiObject2 getComposeMessageEditText() {
+        return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_COMPOSE_MESSAGE_TEXT_ID));
+    }
+
+    private UiObject2 getSendMessageButton() {
+        return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_SEND_MESSAGE_BUTTON_ID));
+    }
+
+    private UiObject2 getMessageRecyclerView() {
+        return mDevice.findObject(By.pkg(UI_PACKAGE_NAME)
+            .clazz("android.support.v7.widget.RecyclerView").res("android", "list"));
+    }
+
+    private UiObject2 getAttachMediaButton() {
+        return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_ATTACH_MEDIA_BUTTON_ID));
+    }
+
+    private UiObject2 getMediaFromDeviceTab() {
+        return mDevice.findObject(By.pkg(UI_PACKAGE_NAME).desc(UI_MEDIA_FROM_DEVICE_DESC));
+    }
+
+    private UiObject2 getMediaGalleryGridView() {
+        return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_MEDIA_GALLERY_GRID_VIEW_ID));
+    }
+
+    private boolean isOnHomePage() {
+        return (getStartNewConversationButton() != null);
+    }
+
+    /*
+     * {@inheritDoc}
+     */
+    @Override
+    public void goToHomePage() {
+        for (int retriesRemaining = 5; retriesRemaining > 0 && !isOnHomePage();
+                --retriesRemaining) {
+            mDevice.pressBack();
+            mDevice.waitForIdle();
+        }
+    }
+
+    /*
+     * {@inheritDoc}
+     */
+    @Override
+    public void goToNewConversationPage() {
+        UiObject2 startNewConversationButton = getStartNewConversationButton();
+        if (startNewConversationButton == null) {
+            throw new IllegalStateException("Could not find start new conversation button");
+        }
+
+        startNewConversationButton.click();
+        if (!mDevice.wait(Until.hasObject(
+                By.res(UI_PACKAGE_NAME, UI_RECIPIENT_TEXT_VIEW_ID)), UI_DIALOG_WAIT)) {
+            throw new UnknownUiException("Could not find recipient text view");
+        }
+    }
+
+    /*
+     * {@inheritDoc}
+     */
+    @Override
+    public void goToMessagesPage() {
+        UiObject2 contact = mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_CONTACT_NAME_ID));
+        if (contact == null) {
+            throw new IllegalStateException("Could not find first contact drop down menu item");
+        }
+
+        contact.click();
+        if (!mDevice.wait(Until.hasObject(
+                By.res(UI_PACKAGE_NAME, UI_COMPOSE_MESSAGE_TEXT_ID)), UI_DIALOG_WAIT)) {
+            throw new UnknownUiException("Could not find compose message edit text");
+        }
+    }
+
+    private void goToFullscreenChooseMediaPage() {
+        UiObject2 mediaGalleryGridView = getMediaGalleryGridView();
+        if (mediaGalleryGridView == null) {
+            throw new IllegalStateException("Could not find media gallery grid view");
+        }
+
+        mediaGalleryGridView.scroll(Direction.DOWN, 5.0f);
+        if (!mDevice.wait(Until.hasObject(By.pkg(UI_PACKAGE_NAME).text(UI_CHOOSE_PHOTO_TEXT)),
+                UI_DIALOG_WAIT)) {
+            throw new UnknownUiException("Could not find full screen media gallery grid view");
+        }
+    }
+
+    /*
+     * {@inheritDoc}
+     */
+    @Override
+    public void scrollMessages(Direction direction) {
+        if (!(Direction.UP.equals(direction) || Direction.DOWN.equals(direction))) {
+            throw new IllegalArgumentException("Direction must be UP or DOWN");
+        }
+
+        UiObject2 messageRecyclerView = getMessageRecyclerView();
+        if (messageRecyclerView == null) {
+            throw new UnknownUiException("Could not find message recycler view");
+        }
+
+        messageRecyclerView.scroll(direction, 10.0f);
+    }
+
+    /*
+     * {@inheritDoc}
+     */
+    @Override
+    public void clickComposeMessageText() {
+        UiObject2 composeMessageEditText = getComposeMessageEditText();
+        if (composeMessageEditText == null) {
+            throw new IllegalStateException("Could not find compose message edit text");
+        }
+
+        composeMessageEditText.click();
+    }
+
+    /*
+     * {@inheritDoc}
+     */
+    @Override
+    public void clickSendMessageButton() {
+        UiObject2 sendMessageButton = getSendMessageButton();
+        if (sendMessageButton == null) {
+            throw new IllegalStateException("Could not find send message button");
+        }
+
+        sendMessageButton.click();
+    }
+
+    private void clickAttachMediaButton() {
+        UiObject2 attachMediaButton = getAttachMediaButton();
+        if (attachMediaButton == null) {
+            throw new IllegalStateException("Could not find attach media button");
+        }
+
+        attachMediaButton.click();
+           if (!mDevice.wait(Until.hasObject(
+                By.res(UI_PACKAGE_NAME, UI_MEDIA_PICKER_TABSTRIP_ID)), UI_DIALOG_WAIT)) {
+            throw new UnknownUiException("Could not find media picker tabstrip");
+        }
+    }
+
+    private void clickMediaFromDeviceTab() {
+        UiObject2 mediaFromDeviceTab = getMediaFromDeviceTab();
+        if (mediaFromDeviceTab == null) {
+            throw new IllegalStateException("Could not find media from device tab");
+        }
+
+        if (!mediaFromDeviceTab.isSelected()) {
+            mediaFromDeviceTab.click();
+            if (!mDevice.wait(Until.hasObject(By.pkg(UI_PACKAGE_NAME).desc(
+                    UI_MEDIA_FROM_DEVICE_DESC).selected(true)), UI_DIALOG_WAIT)) {
+                throw new UnknownUiException("Media from device tab not selected");
+            }
+        }
+    }
+
+    /*
+     * {@inheritDoc}
+     */
+    @Override
+    public void attachMediaFromDevice(int index) {
+        clickAttachMediaButton();
+        clickMediaFromDeviceTab();
+        goToFullscreenChooseMediaPage();
+
+        UiObject2 mediaGalleryGridView = getMediaGalleryGridView();
+        if (mediaGalleryGridView == null) {
+            throw new UnknownUiException("Could not find media gallery grid view");
+        }
+
+        List<UiObject2> mediaGalleryChildren = mediaGalleryGridView.getChildren();
+        if (index < 0 || index >= mediaGalleryChildren.size()) {
+            throw new IndexOutOfBoundsException(String.format("index %d >= size %d",
+                    index, mediaGalleryChildren.size()));
+        }
+
+        int imageChildIndex = 1;
+        UiObject2 imageView = mediaGalleryChildren.get(index).
+                getChildren().get(imageChildIndex);
+        while (getMediaGalleryGridView() != null) {
+            imageView.click();
+            // Needed to prevent StaleObjectException
+            SystemClock.sleep(2000);
+        }
+    }
+}
diff --git a/libraries/launcher-helper/src/android/support/test/launcherhelper/AospLauncherStrategy.java b/libraries/launcher-helper/src/android/support/test/launcherhelper/AospLauncherStrategy.java
index a9009ee..938e6a6 100644
--- a/libraries/launcher-helper/src/android/support/test/launcherhelper/AospLauncherStrategy.java
+++ b/libraries/launcher-helper/src/android/support/test/launcherhelper/AospLauncherStrategy.java
@@ -44,7 +44,7 @@
     @Override
     public void open() {
         // if we see hotseat, assume at home screen already
-        if (!mDevice.hasObject(HOTSEAT)) {
+        if (!mDevice.hasObject(getHotSeatSelector())) {
             mDevice.pressHome();
             Assert.assertTrue("Failed to open launcher",
                     mDevice.wait(Until.hasObject(By.pkg(LAUNCHER_PKG)), 5000));
@@ -69,9 +69,15 @@
             // taps on the "apps" button at the bottom of the screen
             mDevice.findObject(By.desc("Apps")).click();
             // wait until hotseat disappears, so that we know that we are no longer on home screen
-            mDevice.wait(Until.gone(HOTSEAT), 2000);
+            mDevice.wait(Until.gone(getHotSeatSelector()), 2000);
             mDevice.waitForIdle();
         }
+        // check if there's a "cling" on screen
+        UiObject2 cling = mDevice.findObject(By.res(LAUNCHER_PKG, "cling_dismiss")
+                .clazz(Button.class).text("OK"));
+        if (cling != null) {
+            cling.click();
+        }
         // taps on the "apps" page selector near the top of the screen
         UiObject2 appsTab = mDevice.findObject(By.desc("Apps")
                 .clazz(TextView.class).selected(false));
@@ -108,7 +114,7 @@
             // taps on the "apps" button at the bottom of the screen
             mDevice.findObject(By.desc("Apps")).click();
             // wait until hotseat disappears, so that we know that we are no longer on home screen
-            mDevice.wait(Until.gone(HOTSEAT), 2000);
+            mDevice.wait(Until.gone(getHotSeatSelector()), 2000);
             mDevice.waitForIdle();
         }
         // taps on the "Widgets" page selector near the top of the screen
@@ -146,7 +152,7 @@
      * {@inheritDoc}
      */
     @Override
-    public boolean launch(String appName, String packageName) {
+    public long launch(String appName, String packageName) {
         return CommonLauncherHelper.getInstance(mDevice).launchApp(this,
                 By.res("").clazz(TextView.class).desc(appName), packageName);
     }
@@ -195,6 +201,14 @@
      * {@inheritDoc}
      */
     @Override
+    public BySelector getHotSeatSelector() {
+        return HOTSEAT;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
     public Direction getWorkspaceScrollDirection() {
         return Direction.RIGHT;
     }
diff --git a/libraries/launcher-helper/src/android/support/test/launcherhelper/BaseLauncher3Strategy.java b/libraries/launcher-helper/src/android/support/test/launcherhelper/BaseLauncher3Strategy.java
new file mode 100644
index 0000000..40aec1d
--- /dev/null
+++ b/libraries/launcher-helper/src/android/support/test/launcherhelper/BaseLauncher3Strategy.java
@@ -0,0 +1,191 @@
+/*
+ * 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 java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+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.widget.TextView;
+import junit.framework.Assert;
+
+public abstract class BaseLauncher3Strategy implements ILauncherStrategy {
+    private static final String LOG_TAG = BaseLauncher3Strategy.class.getSimpleName();
+    protected UiDevice mDevice;
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void setUiDevice(UiDevice uiDevice) {
+        mDevice = uiDevice;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void open() {
+        // if we see hotseat, assume at home screen already
+        if (!mDevice.hasObject(getHotSeatSelector())) {
+            mDevice.pressHome();
+            // ensure launcher is shown
+            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);
+                }
+                Assert.fail("Failed to open launcher");
+            }
+            mDevice.waitForIdle();
+        }
+        dismissHomeScreenCling();
+    }
+
+    /**
+     * Checks and dismisses home screen cling
+     */
+    protected void dismissHomeScreenCling() {
+        // empty default implementation
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public UiObject2 openAllApps(boolean reset) {
+        // if we see all apps container, skip the opening step
+        if (!mDevice.hasObject(getAllAppsSelector())) {
+            open();
+            // taps on the "apps" button at the bottom of the screen
+            mDevice.findObject(By.desc("Apps")).click();
+            // wait until hotseat disappears, so that we know that we are no longer on home screen
+            mDevice.wait(Until.gone(getHotSeatSelector()), 2000);
+            mDevice.waitForIdle();
+        }
+        UiObject2 allAppsContainer = mDevice.wait(Until.findObject(getAllAppsSelector()), 2000);
+        Assert.assertNotNull("openAllApps: did not find all apps container", allAppsContainer);
+        if (reset) {
+            CommonLauncherHelper.getInstance(mDevice).scrollBackToBeginning(
+                    allAppsContainer, Direction.reverse(getAllAppsScrollDirection()));
+        }
+        return allAppsContainer;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public Direction getAllAppsScrollDirection() {
+        return Direction.DOWN;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public UiObject2 openAllWidgets(boolean reset) {
+        if (!mDevice.hasObject(getAllWidgetsSelector())) {
+            open();
+            // trigger the wallpapers/widgets/settings view
+            mDevice.pressMenu();
+            mDevice.waitForIdle();
+            mDevice.findObject(By.res(getSupportedLauncherPackage(), "widget_button")).click();
+        }
+        UiObject2 allWidgetsContainer = mDevice.wait(
+                Until.findObject(getAllWidgetsSelector()), 2000);
+        Assert.assertNotNull("openAllWidgets: did not find all widgets container",
+                allWidgetsContainer);
+        if (reset) {
+            CommonLauncherHelper.getInstance(mDevice).scrollBackToBeginning(
+                    allWidgetsContainer, Direction.reverse(getAllWidgetsScrollDirection()));
+        }
+        return allWidgetsContainer;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public Direction getAllWidgetsScrollDirection() {
+        return Direction.DOWN;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public long launch(String appName, String packageName) {
+        BySelector app = By.res(
+                getSupportedLauncherPackage(), "icon").clazz(TextView.class).desc(appName);
+        return CommonLauncherHelper.getInstance(mDevice).launchApp(this, app, packageName);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public BySelector getAllAppsSelector() {
+        return By.res(getSupportedLauncherPackage(), "apps_list_view");
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public BySelector getAllWidgetsSelector() {
+        return By.res(getSupportedLauncherPackage(), "widgets_list_view");
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public BySelector getWorkspaceSelector() {
+        return By.res(getSupportedLauncherPackage(), "workspace");
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public BySelector getHotSeatSelector() {
+        return By.res(getSupportedLauncherPackage(), "hotseat");
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public Direction getWorkspaceScrollDirection() {
+        return Direction.RIGHT;
+    }
+}
diff --git a/libraries/launcher-helper/src/android/support/test/launcherhelper/CommonLauncherHelper.java b/libraries/launcher-helper/src/android/support/test/launcherhelper/CommonLauncherHelper.java
index de1776d..1ed33d0 100644
--- a/libraries/launcher-helper/src/android/support/test/launcherhelper/CommonLauncherHelper.java
+++ b/libraries/launcher-helper/src/android/support/test/launcherhelper/CommonLauncherHelper.java
@@ -16,6 +16,8 @@
 package android.support.test.launcherhelper;
 
 import android.graphics.Rect;
+import android.os.RemoteException;
+import android.os.SystemClock;
 import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.BySelector;
 import android.support.test.uiautomator.Direction;
@@ -75,7 +77,7 @@
             attempts++;
             if (attempts > maxAttempts) {
                 throw new RuntimeException(
-                        "scrollBackToBeginning: exceeded max attampts: " + maxAttempts);
+                        "scrollBackToBeginning: exceeded max attempts: " + maxAttempts);
             }
         }
     }
@@ -119,7 +121,7 @@
      * @param packageName
      * @return
      */
-    public boolean launchApp(ILauncherStrategy launcherStrategy, BySelector app,
+    public long launchApp(ILauncherStrategy launcherStrategy, BySelector app,
             String packageName) {
         return launchApp(launcherStrategy, app, packageName, MAX_SCROLL_ATTEMPTS);
     }
@@ -131,10 +133,19 @@
      * @param app
      * @param packageName
      * @param maxScrollAttempts
-     * @return
+     * @return the SystemClock#uptimeMillis timestamp just before launching the application.
      */
-    public boolean launchApp(ILauncherStrategy launcherStrategy, BySelector app,
+    public long launchApp(ILauncherStrategy launcherStrategy, BySelector app,
             String packageName, int maxScrollAttempts) {
+        unlockDeviceIfAsleep();
+
+        if (isAppOpen(packageName)) {
+            // Application is already open
+            return 0;
+        }
+
+        // Go to the home page
+        launcherStrategy.open();
         Direction dir = launcherStrategy.getAllAppsScrollDirection();
         // attempt to find the app icon if it's not already on the screen
         if (!mDevice.hasObject(app)) {
@@ -147,7 +158,7 @@
                     attempts++;
                     if (attempts > maxScrollAttempts) {
                         throw new RuntimeException(
-                                "launchApp: exceeded max attampts to locate app icon: "
+                                "launchApp: exceeded max attempts to locate app icon: "
                                         + maxScrollAttempts);
                     }
                 }
@@ -156,17 +167,43 @@
             ensureIconVisible(app, container, dir);
         }
 
+        long ready = SystemClock.uptimeMillis();
         if (!mDevice.findObject(app).clickAndWait(Until.newWindow(), APP_LAUNCH_TIMEOUT)) {
             Log.w(LOG_TAG, "no new window detected after app launch attempt.");
-            return false;
+            return ILauncherStrategy.LAUNCH_FAILED_TIMESTAMP;
         }
         mDevice.waitForIdle();
         if (packageName != null) {
             Log.w(LOG_TAG, String.format(
                     "No UI element with package name %s detected.", packageName));
-            return mDevice.wait(Until.hasObject(By.pkg(packageName).depth(0)), APP_LAUNCH_TIMEOUT);
+            boolean success = mDevice.wait(Until.hasObject(
+                    By.pkg(packageName).depth(0)), APP_LAUNCH_TIMEOUT);
+            if (success) {
+                return ready;
+            } else {
+                return ILauncherStrategy.LAUNCH_FAILED_TIMESTAMP;
+            }
         } else {
-            return true;
+            return ready;
+        }
+    }
+
+    private boolean isAppOpen (String appPackage) {
+        return mDevice.hasObject(By.pkg(appPackage).depth(0));
+    }
+
+    private 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);
+        }
+        // Check for lock screen element
+        if (mDevice.hasObject(By.res("com.android.systemui", "keyguard_bottom_area"))) {
+            mDevice.pressMenu();
         }
     }
 }
diff --git a/libraries/launcher-helper/src/android/support/test/launcherhelper/GoogleExperienceLauncherStrategy.java b/libraries/launcher-helper/src/android/support/test/launcherhelper/GoogleExperienceLauncherStrategy.java
index bd92b8b..bc78c33 100644
--- a/libraries/launcher-helper/src/android/support/test/launcherhelper/GoogleExperienceLauncherStrategy.java
+++ b/libraries/launcher-helper/src/android/support/test/launcherhelper/GoogleExperienceLauncherStrategy.java
@@ -15,177 +15,15 @@
  */
 package android.support.test.launcherhelper;
 
-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.widget.TextView;
-
-import junit.framework.Assert;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-
 /**
  * Implementation of {@link ILauncherStrategy} to support Google experience launcher
  */
-public class GoogleExperienceLauncherStrategy implements ILauncherStrategy {
+public class GoogleExperienceLauncherStrategy extends BaseLauncher3Strategy {
 
-    private static final String LOG_TAG = GoogleExperienceLauncherStrategy.class.getSimpleName();
     private static final String LAUNCHER_PKG = "com.google.android.googlequicksearchbox";
-    private static final BySelector APPS_CONTAINER = By.res(LAUNCHER_PKG, "all_apps_container");
-    private static final BySelector WIDGETS_CONTAINER = By.res(LAUNCHER_PKG, "widgets_list_view");
-    private static final BySelector WORKSPACE = By.res(LAUNCHER_PKG, "workspace");
-    private static final BySelector HOTSEAT = By.res(LAUNCHER_PKG, "hotseat");
-    private UiDevice mDevice;
 
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void setUiDevice(UiDevice uiDevice) {
-        mDevice = uiDevice;
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void open() {
-        // if we see hotseat, assume at home screen already
-        if (!mDevice.hasObject(HOTSEAT)) {
-            mDevice.pressHome();
-            // ensure launcher is shown
-            if (!mDevice.wait(Until.hasObject(By.res(LAUNCHER_PKG, "hotseat")), 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);
-                }
-                Assert.fail("Failed to open launcher");
-            }
-            mDevice.waitForIdle();
-        }
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public UiObject2 openAllApps(boolean reset) {
-        // if we see all apps container, skip the opening step
-        if (!mDevice.hasObject(APPS_CONTAINER)) {
-            open();
-            // taps on the "apps" button at the bottom of the screen
-            mDevice.findObject(By.desc("Apps")).click();
-            // wait until hotseat disappears, so that we know that we are no longer on home screen
-            mDevice.wait(Until.gone(HOTSEAT), 2000);
-            mDevice.waitForIdle();
-        }
-        UiObject2 allAppsContainer = mDevice.wait(Until.findObject(APPS_CONTAINER), 2000);
-        Assert.assertNotNull("openAllApps: did not find all apps container", allAppsContainer);
-        if (reset) {
-            CommonLauncherHelper.getInstance(mDevice).scrollBackToBeginning(
-                    allAppsContainer, Direction.reverse(getAllAppsScrollDirection()));
-        }
-        return allAppsContainer;
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public Direction getAllAppsScrollDirection() {
-        return Direction.DOWN;
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public UiObject2 openAllWidgets(boolean reset) {
-        if (!mDevice.hasObject(WIDGETS_CONTAINER)) {
-            open();
-            // trigger the wallpapers/widgets/settings view
-            mDevice.pressMenu();
-            mDevice.waitForIdle();
-            mDevice.findObject(By.res(LAUNCHER_PKG, "widget_button")).click();
-        }
-        UiObject2 allWidgetsContainer = mDevice.wait(Until.findObject(WIDGETS_CONTAINER), 2000);
-        Assert.assertNotNull("openAllWidgets: did not find all widgets container",
-                allWidgetsContainer);
-        if (reset) {
-            CommonLauncherHelper.getInstance(mDevice).scrollBackToBeginning(
-                    allWidgetsContainer, Direction.reverse(getAllWidgetsScrollDirection()));
-        }
-        return allWidgetsContainer;
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public Direction getAllWidgetsScrollDirection() {
-        return Direction.DOWN;
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public boolean launch(String appName, String packageName) {
-        BySelector app = By.res(LAUNCHER_PKG, "icon").clazz(TextView.class).desc(appName);
-        return CommonLauncherHelper.getInstance(mDevice).launchApp(this, app, packageName);
-    }
-
-    /**
-     * {@inheritDoc}
-     */
     @Override
     public String getSupportedLauncherPackage() {
         return LAUNCHER_PKG;
     }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public BySelector getAllAppsSelector() {
-        return APPS_CONTAINER;
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public BySelector getAllWidgetsSelector() {
-        return WIDGETS_CONTAINER;
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public BySelector getWorkspaceSelector() {
-        return WORKSPACE;
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public Direction getWorkspaceScrollDirection() {
-        return Direction.RIGHT;
-    }
 }
diff --git a/libraries/launcher-helper/src/android/support/test/launcherhelper/ILauncherStrategy.java b/libraries/launcher-helper/src/android/support/test/launcherhelper/ILauncherStrategy.java
index 301bd1b..bf26bb4 100644
--- a/libraries/launcher-helper/src/android/support/test/launcherhelper/ILauncherStrategy.java
+++ b/libraries/launcher-helper/src/android/support/test/launcherhelper/ILauncherStrategy.java
@@ -27,6 +27,7 @@
  * method.
  */
 public interface ILauncherStrategy {
+    public static final long LAUNCH_FAILED_TIMESTAMP = -1;
 
     /**
      * Returns the launcher application package that this {@link ILauncherStrategy} can automate
@@ -90,6 +91,12 @@
     public BySelector getWorkspaceSelector();
 
     /**
+     * Returns a {@link BySelector} describing the home screen hot seat (app icons at the bottom)
+     * @return
+     */
+    public BySelector getHotSeatSelector();
+
+    /**
      * Retrieves the home screen workspace forward scroll direction as implemented by the launcher
      * @return
      */
@@ -104,5 +111,5 @@
      * @return <code>true</code> if application is verified to be in foreground after launch, or the
      *   verification is skipped; <code>false</code> otherwise.
      */
-    public boolean launch(String appName, String packageName);
+    public long launch(String appName, String packageName);
 }
diff --git a/libraries/launcher-helper/src/android/support/test/launcherhelper/ILeanbackLauncherStrategy.java b/libraries/launcher-helper/src/android/support/test/launcherhelper/ILeanbackLauncherStrategy.java
new file mode 100644
index 0000000..52e3eea
--- /dev/null
+++ b/libraries/launcher-helper/src/android/support/test/launcherhelper/ILeanbackLauncherStrategy.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.support.test.launcherhelper;
+
+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.
+ * <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.
+ */
+public interface ILeanbackLauncherStrategy extends ILauncherStrategy {
+
+    /**
+     * Searches for a given query on leanback launcher
+     */
+    public void search(String query);
+
+    /**
+     * Returns a {@link BySelector} describing the search row
+     * @return
+     */
+    public BySelector getSearchRowSelector();
+
+    /**
+     * Returns a {@link BySelector} describing the notification row (or recommendation row)
+     * @return
+     */
+    public BySelector getNotificationRowSelector();
+
+    /**
+     * Returns a {@link BySelector} describing the apps row
+     * @return
+     */
+    public BySelector getAppsRowSelector();
+
+    /**
+     * Returns a {@link BySelector} describing the games row
+     * @return
+     */
+    public BySelector getGamesRowSelector();
+
+    /**
+     * Returns a {@link BySelector} describing the settings row
+     * @return
+     */
+    public BySelector getSettingsRowSelector();
+
+    /**
+     * Returns a {@link UiObject2} describing the focused search row
+     * @return
+     */
+    public UiObject2 selectSearchRow();
+
+    /**
+     * Returns a {@link UiObject2} describing the focused notification row
+     * @return
+     */
+    public UiObject2 selectNotificationRow();
+
+    /**
+     * Returns a {@link UiObject2} describing the focused apps row
+     * @return
+     */
+    public UiObject2 selectAppsRow();
+
+    /**
+     * Returns a {@link UiObject2} describing the focused games row
+     * @return
+     */
+    public UiObject2 selectGamesRow();
+
+    /**
+     * Returns a {@link UiObject2} describing the focused settings row
+     * @return
+     */
+    public UiObject2 selectSettingsRow();
+}
diff --git a/libraries/launcher-helper/src/android/support/test/launcherhelper/Launcher3Strategy.java b/libraries/launcher-helper/src/android/support/test/launcherhelper/Launcher3Strategy.java
new file mode 100644
index 0000000..60b946c
--- /dev/null
+++ b/libraries/launcher-helper/src/android/support/test/launcherhelper/Launcher3Strategy.java
@@ -0,0 +1,46 @@
+/*
+ * 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.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiObject2;
+
+public class Launcher3Strategy extends BaseLauncher3Strategy {
+
+    private static final String LAUNCHER_PKG = "com.android.launcher3";
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getSupportedLauncherPackage() {
+        return LAUNCHER_PKG;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected void dismissHomeScreenCling() {
+        super.dismissHomeScreenCling();
+        // dismiss first run cling
+        UiObject2 gotItButton = mDevice.findObject(
+                By.res(getSupportedLauncherPackage(), "cling_dismiss_longpress_info"));
+        if (gotItButton != null) {
+            gotItButton.click();
+        }
+    }
+}
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 880638b..f591fe5 100644
--- a/libraries/launcher-helper/src/android/support/test/launcherhelper/LauncherStrategyFactory.java
+++ b/libraries/launcher-helper/src/android/support/test/launcherhelper/LauncherStrategyFactory.java
@@ -41,11 +41,13 @@
         mKnownLauncherStrategies = new HashSet<>();
         registerLauncherStrategy(AospLauncherStrategy.class);
         registerLauncherStrategy(GoogleExperienceLauncherStrategy.class);
+        registerLauncherStrategy(Launcher3Strategy.class);
+        registerLauncherStrategy(LeanbackLauncherStrategy.class);
     }
 
     /**
      * Retrieves an instance of the {@link LauncherStrategyFactory}
-     * @param context
+     * @param uiDevice
      * @return
      */
     public static LauncherStrategyFactory getInstance(UiDevice uiDevice) {
@@ -79,7 +81,7 @@
      * Retrieves a {@link ILauncherStrategy} that supports the current default launcher
      * <p>
      * {@link ILauncherStrategy} maybe registered via
-     * {@link LauncherStrategyRegistry#registerLauncherStrategy(String, Class)} by identifying the
+     * {@link LauncherStrategyFactory#registerLauncherStrategy(Class)} by identifying the
      * launcher package name supported
      * @return
      */
@@ -87,4 +89,17 @@
         String launcherPkg = mUiDevice.getLauncherPackageName();
         return mInstanceMap.get(launcherPkg);
     }
+
+    /**
+     * Retrieves a {@link ILeanbackLauncherStrategy} that supports the current default leanback
+     * launcher
+     * @return
+     */
+    public ILeanbackLauncherStrategy getLeanbackLauncherStrategy() {
+        ILauncherStrategy launcherStrategy = getLauncherStrategy();
+        if (launcherStrategy instanceof ILeanbackLauncherStrategy) {
+            return (ILeanbackLauncherStrategy)launcherStrategy;
+        }
+        throw new RuntimeException("This LauncherStrategy is not for leanback launcher.");
+    }
 }
diff --git a/libraries/launcher-helper/src/android/support/test/launcherhelper/LeanbackLauncherStrategy.java b/libraries/launcher-helper/src/android/support/test/launcherhelper/LeanbackLauncherStrategy.java
new file mode 100644
index 0000000..d3d4c0b
--- /dev/null
+++ b/libraries/launcher-helper/src/android/support/test/launcherhelper/LeanbackLauncherStrategy.java
@@ -0,0 +1,500 @@
+/*
+ * 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.graphics.Point;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.support.test.uiautomator.*;
+import android.util.Log;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+public class LeanbackLauncherStrategy implements ILeanbackLauncherStrategy {
+
+    private static final String LOG_TAG = LeanbackLauncherStrategy.class.getSimpleName();
+    private static final String PACKAGE_LAUNCHER = "com.google.android.leanbacklauncher";
+    private static final String PACKAGE_SEARCH = "com.google.android.katniss";
+
+    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
+
+    protected UiDevice mDevice;
+
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getSupportedLauncherPackage() {
+        return PACKAGE_LAUNCHER;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void setUiDevice(UiDevice uiDevice) {
+        mDevice = uiDevice;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void open() {
+        // if we see main list view, assume at home screen already
+        if (!mDevice.hasObject(getWorkspaceSelector())) {
+            mDevice.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 leanback launcher");
+            }
+            mDevice.waitForIdle();
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public UiObject2 openAllApps(boolean reset) {
+        UiObject2 appsRow = selectAppsRow();
+        if (appsRow == null) {
+            throw new RuntimeException("Could not find all apps row");
+        }
+        if (reset) {
+            Log.w(LOG_TAG, "The reset will be ignored on leanback launcher");
+        }
+        return appsRow;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public BySelector getWorkspaceSelector() {
+        return By.res(getSupportedLauncherPackage(), "main_list_view");
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public BySelector getSearchRowSelector() {
+        return By.res(getSupportedLauncherPackage(), "search_view");
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public BySelector getNotificationRowSelector() {
+        return By.res(getSupportedLauncherPackage(), "notification_view");
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public BySelector getAppsRowSelector() {
+        return By.res(getSupportedLauncherPackage(), "list").desc("Apps");
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public BySelector getGamesRowSelector() {
+        return By.res(getSupportedLauncherPackage(), "list").desc("Games");
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public BySelector getSettingsRowSelector() {
+        return By.res(getSupportedLauncherPackage(), "list").desc("")
+                .hasDescendant(By.res("icon"));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public Direction getAllAppsScrollDirection() {
+        return Direction.RIGHT;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public BySelector getAllAppsSelector() {
+        // On Leanback launcher the Apps row corresponds to the All Apps on phone UI
+        return getAppsRowSelector();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public long launch(String appName, String packageName) {
+        BySelector app = By.res(getSupportedLauncherPackage(), "app_banner").desc(appName);
+        return launchApp(this, app, packageName);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void search(String query) {
+        if (selectSearchRow() == null) {
+            throw new RuntimeException("Could not find search row.");
+        }
+
+        BySelector keyboardOrb = By.res(getSupportedLauncherPackage(), "keyboard_orb");
+        UiObject2 orbButton = mDevice.wait(Until.findObject(keyboardOrb), SHORT_WAIT_TIME);
+        if (orbButton == null) {
+            throw new RuntimeException("Could not find keyboard orb.");
+        }
+        if (orbButton.isFocused()) {
+            mDevice.pressDPadCenter();
+        } else {
+            // Move the focus to keyboard orb by DPad button.
+            mDevice.pressDPadRight();
+            if (orbButton.isFocused()) {
+                mDevice.pressDPadCenter();
+            }
+        }
+        mDevice.wait(Until.gone(keyboardOrb), SHORT_WAIT_TIME);
+
+        BySelector searchEditor = By.res(PACKAGE_SEARCH, "search_text_editor");
+        UiObject2 editText = mDevice.wait(Until.findObject(searchEditor), SHORT_WAIT_TIME);
+        if (editText == null) {
+            throw new RuntimeException("Could not find search text input.");
+        }
+
+        editText.setText(query);
+        SystemClock.sleep(SHORT_WAIT_TIME);
+
+        // Note that Enter key is pressed instead of DPad keys to dismiss leanback IME
+        mDevice.pressEnter();
+        mDevice.wait(Until.gone(searchEditor), SHORT_WAIT_TIME);
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * Assume that the rows are sorted in the following order from the top:
+     *  Search, Notification(, Partner), Apps, Games, Settings(, and Inputs)
+     */
+    @Override
+    public UiObject2 selectNotificationRow() {
+        if (!isNotificationRowSelected()) {
+            open();
+            mDevice.pressHome();    // Home key to move to the first card in the Notification row
+        }
+        return mDevice.wait(Until.findObject(
+                getNotificationRowSelector().hasChild(By.focused(true))), SHORT_WAIT_TIME);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public UiObject2 selectSearchRow() {
+        if (!isSearchRowSelected()) {
+            selectNotificationRow();
+            mDevice.pressDPadUp();
+        }
+        return mDevice.wait(Until.findObject(
+                getSearchRowSelector().hasDescendant(By.focused(true))), SHORT_WAIT_TIME);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public UiObject2 selectAppsRow() {
+        // Start finding Apps row from Notification row
+        if (!isAppsRowSelected()) {
+            selectNotificationRow();
+            mDevice.pressDPadDown();
+        }
+        return mDevice.wait(Until.findObject(
+                getAllAppsSelector().hasChild(By.focused(true))), SHORT_WAIT_TIME);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public UiObject2 selectGamesRow() {
+        if (!isGamesRowSelected()) {
+            selectAppsRow();
+            mDevice.pressDPadDown();
+            // If more than or equal to 16 apps are installed, the app banner could be cut off
+            // into two rows at maximum. It needs to scroll down once more.
+            if (!isGamesRowSelected()) {
+                mDevice.pressDPadDown();
+            }
+        }
+        return mDevice.wait(Until.findObject(
+                getGamesRowSelector().hasChild(By.focused(true))), SHORT_WAIT_TIME);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public UiObject2 selectSettingsRow() {
+        if (!isSettingsRowSelected()) {
+            open();
+            mDevice.pressHome();    // Home key to move to the first card in the Notification row
+            // The Settings row is at the last position
+            final int MAX_ROW_NUMS = 8;
+            for (int i = 0; i < MAX_ROW_NUMS; ++i) {
+                mDevice.pressDPadDown();
+            }
+        }
+        return null;
+    }
+
+    @SuppressWarnings("unused")
+    @Override
+    public UiObject2 openAllWidgets(boolean reset) {
+        throw new UnsupportedOperationException(
+                "All Widgets is not available on Leanback Launcher.");
+    }
+
+    @SuppressWarnings("unused")
+    @Override
+    public BySelector getAllWidgetsSelector() {
+        throw new UnsupportedOperationException(
+                "All Widgets is not available on Leanback Launcher.");
+    }
+
+    @SuppressWarnings("unused")
+    @Override
+    public Direction getAllWidgetsScrollDirection() {
+        throw new UnsupportedOperationException(
+                "All Widgets is not available on Leanback Launcher.");
+    }
+
+    @SuppressWarnings("unused")
+    @Override
+    public BySelector getHotSeatSelector() {
+        throw new UnsupportedOperationException(
+                "Hot Seat is not available on Leanback Launcher.");
+    }
+
+    @SuppressWarnings("unused")
+    @Override
+    public Direction getWorkspaceScrollDirection() {
+        throw new UnsupportedOperationException(
+                "Workspace is not available on Leanback Launcher.");
+    }
+
+    protected long launchApp(ILauncherStrategy launcherStrategy, BySelector app,
+            String packageName) {
+        return launchApp(launcherStrategy, app, packageName, MAX_SCROLL_ATTEMPTS);
+    }
+
+    protected long launchApp(ILauncherStrategy launcherStrategy, BySelector app,
+            String packageName, int maxScrollAttempts) {
+        unlockDeviceIfAsleep();
+
+        if (isAppOpen(packageName)) {
+            // Application is already open
+            return 0;
+        }
+
+        // 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);
+        UiObject2 appIcon = container.findObject(app);
+        int attempts = 0;
+        while (attempts++ < maxScrollAttempts) {
+            // Compare the focused icon and the app icon to search for.
+            UiObject2 focusedIcon = container.findObject(By.focused(true))
+                    .findObject(By.res(getSupportedLauncherPackage(), "app_banner"));
+
+            if (appIcon == null) {
+                appIcon = findApp(container, focusedIcon, app);
+                if (appIcon == null) {
+                    throw new RuntimeException("Failed to find the app icon on screen: "
+                            + packageName);
+                }
+                continue;
+            } else if (focusedIcon.equals(appIcon)) {
+                // The app icon is on the screen, and selected.
+                break;
+            } else {
+                // The app icon is on the screen, but not selected yet
+                // Move one step closer to the app icon
+                Point currentPosition = focusedIcon.getVisibleCenter();
+                Point targetPosition = appIcon.getVisibleCenter();
+                int dx = targetPosition.x - currentPosition.x;
+                int dy = targetPosition.y - currentPosition.y;
+                final int MARGIN = 10;
+                // The sequence of moving should be kept in the following order so as not to
+                // be stuck in case that the apps row are not even.
+                if (dx < -MARGIN) {
+                    mDevice.pressDPadLeft();
+                    continue;
+                }
+                if (dy < -MARGIN) {
+                    mDevice.pressDPadUp();
+                    continue;
+                }
+                if (dx > MARGIN) {
+                    mDevice.pressDPadRight();
+                    continue;
+                }
+                if (dy > MARGIN) {
+                    mDevice.pressDPadDown();
+                    continue;
+                }
+                throw new RuntimeException(
+                        "Failed to navigate to the app icon on screen: " + packageName);
+            }
+        }
+
+        if (attempts == maxScrollAttempts) {
+            throw new RuntimeException(
+                    "scrollBackToBeginning: exceeded max attempts: " + maxScrollAttempts);
+        }
+
+        // The app icon is already found and focused.
+        long ready = SystemClock.uptimeMillis();
+        mDevice.pressDPadCenter();
+        mDevice.waitForIdle();
+        if (packageName != null) {
+            Log.w(LOG_TAG, String.format(
+                    "No UI element with package name %s detected.", packageName));
+            boolean success = mDevice.wait(Until.hasObject(
+                    By.pkg(packageName).depth(0)), APP_LAUNCH_TIMEOUT);
+            if (success) {
+                return ready;
+            } else {
+                return ILauncherStrategy.LAUNCH_FAILED_TIMESTAMP;
+            }
+        } else {
+            return ready;
+        }
+    }
+
+    protected boolean isSearchRowSelected() {
+        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() {
+        UiObject2 row = mDevice.findObject(getGamesRowSelector());
+        if (row == null) {
+            return false;
+        }
+        return row.hasObject(By.focused(true));
+    }
+
+    protected boolean isNotificationRowSelected() {
+        UiObject2 row = mDevice.findObject(getNotificationRowSelector());
+        if (row == null) {
+            return false;
+        }
+        return row.hasObject(By.focused(true));
+    }
+
+    protected boolean isSettingsRowSelected() {
+        // Settings label is only visible if the settings row is selected
+        return mDevice.hasObject(By.res(getSupportedLauncherPackage(), "label").text("Settings"));
+    }
+
+    protected boolean isAppOpen (String appPackage) {
+        return mDevice.hasObject(By.pkg(appPackage).depth(0));
+    }
+
+    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);
+        }
+    }
+
+    protected UiObject2 findApp(UiObject2 container, UiObject2 focusedIcon, BySelector app) {
+        UiObject2 appIcon;
+        // The app icon is not on the screen.
+        // Search by going left first until it finds the app icon on the screen
+        String prevText = focusedIcon.getContentDescription();
+        String nextText;
+        do {
+            mDevice.pressDPadLeft();
+            appIcon = container.findObject(app);
+            if (appIcon != null) {
+                return appIcon;
+            }
+            nextText = container.findObject(By.focused(true)).findObject(
+                    By.res(getSupportedLauncherPackage(),
+                            "app_banner")).getContentDescription();
+        } while (nextText != null && !nextText.equals(prevText));
+
+        // If we haven't found it yet, search by going right
+        do {
+            mDevice.pressDPadRight();
+            appIcon = container.findObject(app);
+            if (appIcon != null) {
+                return appIcon;
+            }
+            nextText = container.findObject(By.focused(true)).findObject(
+                    By.res(getSupportedLauncherPackage(),
+                            "app_banner")).getContentDescription();
+        } while (nextText != null && !nextText.equals(prevText));
+        return null;
+    }
+}
diff --git a/libraries/maps-app-helper/Android.mk b/libraries/maps-app-helper/Android.mk
new file mode 100644
index 0000000..74259a3
--- /dev/null
+++ b/libraries/maps-app-helper/Android.mk
@@ -0,0 +1,23 @@
+#
+# 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.
+#
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := maps-app-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator base-app-helpers
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/maps-app-helper/src/android/platform/test/helpers/MapsHelperImpl.java b/libraries/maps-app-helper/src/android/platform/test/helpers/MapsHelperImpl.java
new file mode 100644
index 0000000..a24b5a3
--- /dev/null
+++ b/libraries/maps-app-helper/src/android/platform/test/helpers/MapsHelperImpl.java
@@ -0,0 +1,397 @@
+/*
+ * 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.pm.PackageManager.NameNotFoundException;
+import android.os.SystemClock;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.Until;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.util.Log;
+
+import java.util.regex.Pattern;
+
+
+public class MapsHelperImpl extends AbstractMapsHelper {
+    private static final String LOG_TAG = MapsHelperImpl.class.getSimpleName();
+
+    private static final String UI_CLOSE_NAVIGATION_DESC = "Close navigation";
+    private static final String UI_DIRECTIONS_BUTTON_ID = "placepage_directions_button";
+    private static String UI_PACKAGE;
+    private static final String UI_START_NAVIGATION_BUTTON_ID = "start_button";
+    private static final String UI_TEXTVIEW_CLASS = "android.widget.TextView";
+    private static final String UI_PROGRESSBAR_CLASS = "android.widget.ProgressBar";
+
+    private static final int UI_RESPONSE_WAIT = 5000;
+    private static final int SEARCH_RESPONSE_WAIT = 25000;
+    private static final int MAP_SERVER_CONNECT_WAIT = 120000;
+
+    private boolean mIsVersion9p30;
+
+    private static final int MAX_CONNECT_TO_SERVER_RETRY = 5;
+    private static final int MAX_START_NAV_RETRY = 5;
+    private static final int MAX_DISMISS_INITIAL_DIALOG_RETRY = 2;
+
+    public MapsHelperImpl(Instrumentation instr) {
+        super(instr);
+
+        try {
+            mIsVersion9p30 = getVersion().startsWith("9.30.");
+            if (mIsVersion9p30) {
+                UI_PACKAGE = "com.google.android.apps.maps";
+            } else {
+                UI_PACKAGE = "com.google.android.apps.gmm";
+            }
+        } catch (NameNotFoundException e) {
+            Log.e(LOG_TAG, String.format("Unable to find package by name, %s", getPackage()));
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getPackage() {
+        return "com.google.android.apps.maps";
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getLauncherName() {
+        return "Maps";
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void dismissInitialDialogs() {
+        Log.v(LOG_TAG, "Maps dismissing initial welcome screen.");
+
+        // ToS welcome dialog
+        boolean successTosDismiss = hasSearchBar(0);
+
+        String text = "ACCEPT & CONTINUE";
+        Pattern pattern = Pattern.compile(text, Pattern.CASE_INSENSITIVE);
+
+        UiObject2 terms = mDevice.wait(Until.findObject(By.text(pattern)), 10000);
+        int tryCounter = 0;
+
+        while ((terms != null) && (tryCounter < MAX_DISMISS_INITIAL_DIALOG_RETRY)) {
+            terms.click();
+
+            mDevice.wait(Until.gone(By.pkg(UI_PACKAGE).clazz(UI_PROGRESSBAR_CLASS)),
+                         MAP_SERVER_CONNECT_WAIT);
+
+            if (!checkServerConnectivity()) {
+                throw new IllegalStateException("Unable to connect to Google Maps server");
+            }
+
+            terms = mDevice.wait(Until.findObject(By.text(pattern)), UI_RESPONSE_WAIT);
+            tryCounter += 1;
+        }
+
+        if (terms != null) {
+            throw new IllegalStateException("Unable to dismiss initial dialogs");
+        }
+
+        if (mIsVersion9p30) {
+            exit();
+            open();
+        }
+
+        // Location services dialog
+        text = "YES, I'M IN";
+        pattern = Pattern.compile(text, Pattern.CASE_INSENSITIVE);
+        UiObject2 location = mDevice.wait(Until.findObject(By.text(pattern)),
+                                          UI_RESPONSE_WAIT);
+        if (location != null) {
+            location.click();
+            mDevice.waitForIdle();
+        } else {
+            Log.e(LOG_TAG, "Did not find a location services dialog.");
+        }
+
+        if (!mIsVersion9p30) {
+            // Tap here dialog
+            UiObject2 cling = mDevice.wait(
+                                Until.findObject(By.res(UI_PACKAGE, "tapherehint_textbox")),
+                                UI_RESPONSE_WAIT);
+            if (cling != null) {
+                cling.click();
+                mDevice.waitForIdle();
+            } else {
+                Log.e(LOG_TAG, "Did not find 'tap here' dialog");
+            }
+
+            // Reset map dialog
+            UiObject2 resetView = mDevice.wait(
+                                     Until.findObject(By.res(UI_PACKAGE, "mylocation_button")),
+                                     UI_RESPONSE_WAIT);
+            if (resetView != null) {
+                resetView.click();
+                mDevice.waitForIdle();
+            } else {
+                Log.e(LOG_TAG, "Did not find 'reset map' dialog.");
+            }
+        }
+
+        // 'Side menu' dialog
+        text = "GOT IT";
+        pattern = Pattern.compile(text, Pattern.CASE_INSENSITIVE);
+        BySelector gotIt = By.text(Pattern.compile("GOT IT", Pattern.CASE_INSENSITIVE));
+        UiObject2 sideMenuTut = mDevice.wait(Until.findObject(gotIt), 5000);
+        if (sideMenuTut != null) {
+            sideMenuTut.click();
+        } else {
+            Log.e(LOG_TAG, "Did not find any 'side menu' dialog.");
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void doSearch(String query) {
+        Log.v(LOG_TAG, "Maps doing an address search");
+
+        if (!checkServerConnectivity()) {
+            throw new IllegalStateException("Cannot connect to Google Maps servers");
+        }
+
+        // Navigate if necessary
+        goToQueryScreen();
+        // Select search bar
+        UiObject2 searchSelect = getSelectableSearchBar(0);
+        if (searchSelect == null) {
+            throw new IllegalStateException("No selectable search bar found.");
+        }
+        searchSelect.click();
+
+        // Edit search query
+        UiObject2 searchEdit = getEditableSearchBar(UI_RESPONSE_WAIT);
+        if (searchEdit == null) {
+            throw new IllegalStateException("Not editable search bar found.");
+        }
+        searchEdit.clear();
+        searchEdit.setText(query);
+
+        // Search and wait for the directions option
+        UiObject2 firstAddressResult = mDevice.wait(Until.findObject(By.pkg(UI_PACKAGE).clazz(
+            UI_TEXTVIEW_CLASS)), SEARCH_RESPONSE_WAIT);
+        if (firstAddressResult == null) {
+            String err_msg = String.format("Did not detect address result after %d seconds",
+                                           (int) Math.floor(SEARCH_RESPONSE_WAIT / 1000));
+            throw new IllegalStateException(err_msg);
+        }
+        firstAddressResult.click();
+
+        if (getDirectionsButton(SEARCH_RESPONSE_WAIT) == null) {
+            throw new IllegalStateException("Could not find directions button");
+        }
+    }
+
+    /*
+     * {@inheritDoc}
+     */
+    @Override
+    public void getDirections() {
+        Log.v(LOG_TAG, "Maps getting direction.");
+
+        dismissPullUpDialog();
+
+        UiObject2 directionsButton = getDirectionsButton(UI_RESPONSE_WAIT);
+        if (directionsButton == null) {
+            throw new IllegalStateException("Unable to find start direction button");
+        }
+        directionsButton.click();
+
+        dismissGetARidePopUp();
+        if (getStartNavigationButton(UI_RESPONSE_WAIT) == null) {
+            throw new IllegalStateException("Unable to find start navigation button");
+        }
+    }
+
+    /*
+     * {@inheritDoc}
+     */
+    @Override
+    public void startNavigation() {
+        Log.v(LOG_TAG, "starting navigation.");
+
+        UiObject2 startNavigationButton = getStartNavigationButton(UI_RESPONSE_WAIT);
+
+        if (startNavigationButton == null) {
+            dismissGetARidePopUp();
+            startNavigationButton = getStartNavigationButton(UI_RESPONSE_WAIT);
+
+            if (startNavigationButton == null) {
+                throw new IllegalStateException("Unable to find start navigation button");
+            }
+        }
+        startNavigationButton.click();
+
+        boolean hasCloseNavigationDesc = (getCloseNavigationButton(UI_RESPONSE_WAIT) != null);
+        int tryCounter = 0;
+        while ((tryCounter < MAX_START_NAV_RETRY) && (!hasCloseNavigationDesc)) {
+            dismissBetaUseDialog();
+            dismissSearchAlongRoutePopUp();
+            hasCloseNavigationDesc = (getCloseNavigationButton(UI_RESPONSE_WAIT) != null);
+            tryCounter += 1;
+        }
+
+        if (!hasCloseNavigationDesc) {
+            throw new IllegalStateException("Unable to find close navigation button");
+        }
+    }
+
+    /*
+     * {@inheritDoc}
+     */
+    @Override
+    public void stopNavigation() {
+        Log.v(LOG_TAG, "stopping navigation.");
+
+        dismissSearchAlongRoutePopUp();
+
+        UiObject2 closeNavigationButton = getCloseNavigationButton(0);
+
+        if (closeNavigationButton != null) {
+            closeNavigationButton.click();
+        }
+
+        if (hasNavigationButton(UI_RESPONSE_WAIT)) {
+            mDevice.pressBack();
+        }
+    }
+
+    private void goToQueryScreen() {
+        for (int backup = 5; backup > 0; backup--) {
+            if (hasSearchBar(0)) {
+                return;
+            } else {
+                mDevice.pressBack();
+                mDevice.waitForIdle();
+            }
+        }
+    }
+
+    private UiObject2 getSelectableSearchBar(int wait_time) {
+        return mDevice.wait(Until.findObject(By.res(UI_PACKAGE, "search_omnibox_text_box")),
+                            wait_time);
+    }
+
+    private UiObject2 getEditableSearchBar(int wait_time) {
+        return mDevice.wait(Until.findObject(By.res(UI_PACKAGE, "search_omnibox_edit_text")),
+                            wait_time);
+    }
+
+    private UiObject2 getStartNavigationButton(int wait_time) {
+        return mDevice.wait(Until.findObject(By.res(UI_PACKAGE, UI_START_NAVIGATION_BUTTON_ID)),
+                            wait_time);
+    }
+
+    private UiObject2 getCloseNavigationButton(int wait_time) {
+        return mDevice.wait(Until.findObject(By.pkg(UI_PACKAGE).desc(UI_CLOSE_NAVIGATION_DESC)),
+                            wait_time);
+    }
+
+    private UiObject2 getDirectionsButton(int wait_time) {
+        return mDevice.wait(Until.findObject(By.res(UI_PACKAGE, UI_DIRECTIONS_BUTTON_ID)),
+                            wait_time);
+    }
+
+    private boolean hasSearchBar(int wait_time) {
+        return ((getSelectableSearchBar(wait_time) != null) ||
+                (getEditableSearchBar(wait_time) != null));
+    }
+
+    private boolean hasNavigationButton(int wait_time) {
+        return ((getStartNavigationButton(wait_time) != null) ||
+                (getDirectionsButton(wait_time) != null));
+    }
+
+    // check connectivity issues by looking for "TRY AGAIN" pop-up dialog
+    private boolean checkServerConnectivity() {
+        int tryCounter = 0;
+
+        UiObject2 tryAgainButton = mDevice.wait(Until.findObject(By.text("TRY AGAIN")),
+                                                UI_RESPONSE_WAIT);
+        while ((tryCounter < MAX_CONNECT_TO_SERVER_RETRY) && (tryAgainButton != null)) {
+            tryAgainButton.click();
+
+            tryAgainButton = mDevice.wait(Until.findObject(By.text("TRY AGAIN")),
+                                          MAP_SERVER_CONNECT_WAIT);
+            tryCounter += 1;
+        }
+
+        if (tryAgainButton != null) {
+            return false;
+        }
+        else {
+            return true;
+        }
+    }
+
+    // Dismiss pop up dialog with title "Google Maps Navigation is in beta.  Use caution"
+    private void dismissBetaUseDialog() {
+        UiObject2 acceptButton = mDevice.wait(
+                                   Until.findObject(By.text("ACCEPT")),
+                                   UI_RESPONSE_WAIT);
+        if (acceptButton != null) {
+            acceptButton.click();
+            mDevice.wait(Until.gone(By.text("ACCEPT")), UI_RESPONSE_WAIT);
+        }
+    }
+
+    // Dismiss pop-up dialog with title "Search along route"
+    private void dismissSearchAlongRoutePopUp() {
+        UiObject2 searchAlongRoute = mDevice.wait(
+                                       Until.findObject(By.textContains("Search along route")),
+                                       UI_RESPONSE_WAIT);
+        if (searchAlongRoute != null) {
+            mDevice.pressBack();
+        }
+    }
+
+    // Dismiss pop-up dialog with title "Pull up"
+    private void dismissPullUpDialog() {
+        UiObject2 gotItButton = mDevice.wait(
+                                  Until.findObject(By.text("GOT IT")),
+                                  UI_RESPONSE_WAIT);
+        if (gotItButton != null) {
+            gotItButton.click();
+            mDevice.wait(Until.gone(By.text("GOT IT")), UI_RESPONSE_WAIT);
+        }
+    }
+
+    // Dismiss pop-up advertising for taxi-ride with title "Get a ride in minutes"
+    private void dismissGetARidePopUp() {
+        UiObject2 getARide = mDevice.wait(
+                               Until.findObject(By.textContains("Get a ride in minutes")),
+                               UI_RESPONSE_WAIT);
+        if (getARide != null) {
+            mDevice.pressBack();
+        }
+    }
+}
diff --git a/libraries/photos-app-helper/Android.mk b/libraries/photos-app-helper/Android.mk
new file mode 100644
index 0000000..6d3dcba
--- /dev/null
+++ b/libraries/photos-app-helper/Android.mk
@@ -0,0 +1,23 @@
+#
+# 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.
+#
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := photos-app-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator base-app-helpers
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/photos-app-helper/src/android/platform/test/helpers/PhotosHelperImpl.java b/libraries/photos-app-helper/src/android/platform/test/helpers/PhotosHelperImpl.java
new file mode 100644
index 0000000..adbabb1
--- /dev/null
+++ b/libraries/photos-app-helper/src/android/platform/test/helpers/PhotosHelperImpl.java
@@ -0,0 +1,509 @@
+/*
+ * 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.pm.PackageManager.NameNotFoundException;
+import android.os.SystemClock;
+import android.platform.test.helpers.exceptions.UnknownUiException;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.Until;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.util.Log;
+
+import java.util.List;
+import java.util.regex.Pattern;
+
+import junit.framework.Assert;
+
+
+import android.os.Environment;
+import java.io.File;
+import java.io.IOException;
+
+public class PhotosHelperImpl extends AbstractPhotosHelper {
+    private static final String LOG_TAG = PhotosHelperImpl.class.getSimpleName();
+
+    private static final long APP_LOAD_WAIT = 7500;
+    private static final long HACKY_WAIT = 2500;
+    private static final long PICTURE_LOAD_WAIT = 20000;
+    private static final long UI_NAVIGATION_WAIT = 5000;
+
+    private static final Pattern UI_PHOTO_DESC = Pattern.compile("^Photo.*");
+
+    private static final String UI_DONE_BUTTON_ID = "done_button";
+    private static final String UI_GET_STARTED_CONTAINER = "get_started_container";
+    private static final String UI_GET_STARTED_ID = "get_started";
+    private static final String UI_LOADING_ICON_ID = "list_empty_progress_bar";
+    private static final String UI_NEXT_BUTTON_ID = "next_button";
+    private static final String UI_PACKAGE_NAME = "com.google.android.apps.photos";
+    private static final String UI_PHOTO_TAB_ID = "tab_photos";
+    private static final String UI_DEVICE_FOLDER_TEXT = "Device folders";
+    private static final String UI_PHOTO_VIEW_PAGER_ID = "photo_view_pager";
+    private static final String UI_PHOTO_SCROLL_VIEW_ID = "recycler_view";
+    private static final String UI_NAVIGATION_LIST_ID = "navigation_list";
+    private static final int MAX_UI_SCROLL_COUNT = 20;
+    private static final int MAX_DISMISS_INIT_DIALOG_RETRY = 20;
+
+    public PhotosHelperImpl(Instrumentation instr) {
+        super(instr);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getPackage() {
+        return "com.google.android.apps.photos";
+    }
+
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getLauncherName() {
+        return "Photos";
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void dismissInitialDialogs() {
+        // Target Photos version 1.18.0.119671374
+        SystemClock.sleep(APP_LOAD_WAIT);
+
+        if (isOnInitialDialogScreen()) {
+            UiObject2 getStartedButton = mDevice.wait(
+                    Until.findObject(By.res(UI_PACKAGE_NAME, UI_GET_STARTED_ID)), APP_LOAD_WAIT);
+            int retryCount = 0;
+            while ((retryCount < MAX_DISMISS_INIT_DIALOG_RETRY) &&
+                   (getStartedButton == null)) {
+                /*
+                  The UiAutomator sometimes cannot find GET STARTED button even though
+                  it is seen on the screen.
+                  The reason is because the initial "spinner" animation screen updates
+                  views too quickly for UiAutomator to catch the change.
+
+                  The following hack is used to reload the init dialog for UiAutomator to
+                  retry catching the GET STARTED button.
+                */
+
+                mDevice.pressBack();
+                mDevice.waitForIdle();
+                mDevice.pressHome();
+                mDevice.waitForIdle();
+                open();
+
+                getStartedButton = mDevice.wait(
+                        Until.findObject(By.res(UI_PACKAGE_NAME, UI_GET_STARTED_ID)),
+                        APP_LOAD_WAIT);
+                retryCount += 1;
+
+                if (!isOnInitialDialogScreen()) {
+                    break;
+                }
+            }
+
+            if (isOnInitialDialogScreen() && (getStartedButton == null)) {
+                throw new IllegalStateException("UiAutomator cannot catch GET STARTED button");
+            }
+            else {
+                if (getStartedButton != null) {
+                    getStartedButton.click();
+                }
+            }
+        }
+        else {
+            Log.e(LOG_TAG, "Didn't find GET STARTED button.");
+        }
+
+        // Address dialogs with an account vs. without an account
+        Pattern signInWords = Pattern.compile("Sign in", Pattern.CASE_INSENSITIVE);
+        boolean hasAccount = !mDevice.hasObject(By.text(signInWords));
+        if (!hasAccount) {
+            // Select 'NO THANKS' if no account exists
+            Pattern noThanksWords = Pattern.compile("No thanks", Pattern.CASE_INSENSITIVE);
+            UiObject2 noThanksButton = mDevice.findObject(By.text(noThanksWords));
+            if (noThanksButton != null) {
+                noThanksButton.click();
+                mDevice.waitForIdle();
+            } else {
+                Log.e(LOG_TAG, "Unable to find NO THANKS button.");
+            }
+        } else {
+            UiObject2 doneButton = mDevice.wait(Until.findObject(
+                    By.res(UI_PACKAGE_NAME, UI_DONE_BUTTON_ID)), 5000);
+            if (doneButton != null) {
+                doneButton.click();
+                mDevice.waitForIdle();
+            }
+            else {
+                Log.e(LOG_TAG, "Didn't find DONE button.");
+            }
+
+            // Press the next button (arrow and check mark) four consecutive times
+            for (int repeat = 0; repeat < 4; repeat++) {
+                UiObject2 nextButton = mDevice.findObject(
+                        By.res(UI_PACKAGE_NAME, UI_NEXT_BUTTON_ID));
+                if (nextButton != null) {
+                    nextButton.click();
+                    mDevice.waitForIdle();
+                } else {
+                    Log.e(LOG_TAG, "Unable to find arrow or check mark buttons.");
+                }
+            }
+
+            mDevice.wait(Until.gone(
+                         By.res(UI_PACKAGE_NAME, UI_LOADING_ICON_ID)), PICTURE_LOAD_WAIT);
+        }
+
+        mDevice.waitForIdle();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void openFirstClip() {
+        if (searchForVideoClip()) {
+            UiObject2 clip = getFirstClip();
+            if (clip != null) {
+                clip.click();
+                mDevice.wait(Until.findObject(
+                        By.res(UI_PACKAGE_NAME, "photos_videoplayer_play_button_holder")), 2000);
+            }
+            else {
+                throw new IllegalStateException("Cannot play a video after finding video clips");
+            }
+        }
+        else {
+            throw new UnsupportedOperationException("Cannot find a video clip");
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void pauseClip() {
+        UiObject2 holder = mDevice.findObject(
+                By.res(UI_PACKAGE_NAME, "photos_videoplayer_play_button_holder"));
+        if (holder != null) {
+            holder.click();
+        } else {
+            throw new UnknownUiException("Unable to find pause button holder.");
+        }
+
+        UiObject2 pause = mDevice.wait(Until.findObject(
+                By.res(UI_PACKAGE_NAME, "photos_videoplayer_pause_button")), 2500);
+        if (pause != null) {
+            pause.click();
+            mDevice.wait(Until.findObject(By.desc("Play video")), 2500);
+        } else {
+            throw new UnknownUiException("Unable to find pause button.");
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void playClip() {
+        UiObject2 play = mDevice.findObject(By.desc("Play video"));
+        if (play != null) {
+            play.click();
+            mDevice.wait(Until.findObject(
+                    By.res(UI_PACKAGE_NAME, "photos_videoplayer_pause_button")), 2500);
+        } else {
+            throw new UnknownUiException("Unable to find play button");
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void goToMainScreen() {
+        for (int retriesRemaining = 5; retriesRemaining > 0 && !isOnMainScreen();
+                --retriesRemaining) {
+            // check if we see the Photos tab at the bottom of the screen
+            // If we do, clicking on the tab should go to home screen.
+            UiObject2 photosButton = mDevice.findObject(
+                    By.res(UI_PACKAGE_NAME, UI_PHOTO_TAB_ID));
+            if (photosButton != null) {
+                photosButton.click();
+            }
+            else {
+                mDevice.pressBack();
+            }
+            mDevice.waitForIdle();
+        }
+
+        if (!isOnMainScreen()) {
+            throw new IllegalStateException("Cannot go to main screen");
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void openPicture(int index) {
+
+        mDevice.waitForIdle();
+        List<UiObject2> photos = mDevice.findObjects(By.pkg(UI_PACKAGE_NAME).desc(UI_PHOTO_DESC));
+
+        if (photos == null) {
+            throw new IllegalStateException("Cannot find photos on current view screen");
+        }
+
+        if ((index < 0) || (index >= photos.size())) {
+            String errMsg = String.format("Photo index (%d) out of bound (0..%d)",
+                                          index, photos.size());
+            throw new IllegalArgumentException(errMsg);
+        }
+
+        UiObject2 photo = photos.get(index);
+        photo.click();
+        if (!mDevice.wait(Until.hasObject(By.res(UI_PACKAGE_NAME, UI_PHOTO_VIEW_PAGER_ID)),
+                UI_NAVIGATION_WAIT)) {
+            throw new IllegalStateException("Cannot display photo on screen");
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void scrollAlbum(Direction direction) {
+        if (!(Direction.LEFT.equals(direction) || Direction.RIGHT.equals(direction))) {
+            throw new IllegalArgumentException("Scroll direction must be LEFT or RIGHT");
+        }
+
+        UiObject2 scrollContainer = mDevice.findObject(
+                By.res(UI_PACKAGE_NAME, UI_PHOTO_VIEW_PAGER_ID));
+
+        if (scrollContainer == null) {
+            throw new UnknownUiException("Cannot find scroll container");
+        }
+
+        scrollContainer.scroll(direction, 1.0f);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void goToDeviceFolderScreen() {
+        if (!isOnDeviceFolderScreen()) {
+
+            if (!isOnMainScreen()) {
+                goToMainScreen();
+            }
+
+            openNavigationDrawer();
+
+            UiObject2 deviceFolderButton = mDevice.wait(Until.findObject(
+                                               By.text(UI_DEVICE_FOLDER_TEXT)), UI_NAVIGATION_WAIT);
+            if (deviceFolderButton != null) {
+                deviceFolderButton.click();
+            }
+            else {
+                UiObject2 photosButton = mDevice.wait(Until.findObject(By.text("Photos")),
+                                                      UI_NAVIGATION_WAIT);
+                if (photosButton != null) {
+                    photosButton.click();
+                }
+                else {
+                    throw new IllegalStateException("No device folder in navigation drawer");
+                }
+            }
+        }
+
+        if (!isOnDeviceFolderScreen()) {
+            throw new UnknownUiException("Can not go to device folder screen");
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean searchForDeviceFolder(String folderName) {
+        boolean foundFolder = false;
+        int scrollCount = 0;
+        while (!foundFolder && (scrollCount < MAX_UI_SCROLL_COUNT)) {
+            foundFolder = mDevice.wait(Until.hasObject(By.text(folderName)), 2000);
+            if (!foundFolder) {
+                if (!scrollView(Direction.DOWN)) {
+                    break;
+                }
+            }
+            scrollCount += 1;
+        }
+
+        if (!foundFolder) {
+            foundFolder = mDevice.wait(Until.hasObject(By.text(folderName)), 2000);
+        }
+
+        return foundFolder;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean searchForVideoClip() {
+        boolean foundVideoClip = false;
+        int scrollCount = 0;
+        while (!foundVideoClip && (scrollCount < MAX_UI_SCROLL_COUNT)) {
+            foundVideoClip = (getFirstClip() != null);
+            if (!foundVideoClip) {
+                if (!scrollView(Direction.DOWN)) {
+                    break;
+                }
+            }
+            scrollCount += 1;
+        }
+        return foundVideoClip;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean searchForPicture() {
+        boolean foundPicture = false;
+        int scrollCount = 0;
+        while (!foundPicture && (scrollCount < MAX_UI_SCROLL_COUNT)) {
+            foundPicture = mDevice.wait(Until.hasObject(By.descStartsWith("Photo")), 2000);
+            if (!foundPicture) {
+                if (!scrollView(Direction.DOWN)) {
+                    break;
+                }
+            }
+            scrollCount += 1;
+        }
+        return foundPicture;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void openDeviceFolder(String folderName) {
+        UiObject2 deviceFolder = mDevice.wait(Until.findObject(By.text(folderName)),
+                                              UI_NAVIGATION_WAIT);
+        if (deviceFolder != null) {
+            deviceFolder.click();
+        }
+        else {
+            throw new IllegalArgumentException(String.format("Cannot open device folder %s",
+                                                             folderName));
+        }
+    }
+
+    private UiObject2 getFirstClip() {
+        return mDevice.wait(Until.findObject(By.descStartsWith("Video")), 2000);
+    }
+
+    /**
+     *  This function returns true if Photos is currently on the first-use
+     *  initial dialog screen, with "Get Started" button displayed on screen
+     *
+     * @return Returns true if app is on the initial dialog screen, false otherwise
+     */
+    private boolean isOnInitialDialogScreen() {
+        return mDevice.hasObject(By.res(UI_PACKAGE_NAME, UI_GET_STARTED_CONTAINER));
+    }
+
+    private boolean isOnMainScreen() {
+        return mDevice.hasObject(By.descContains("Photos, selected"));
+    }
+
+    /**
+     *  This function returns true if Photos is currently in the
+     *  photo-viewing screen, displaying either one photo
+     *  or video on the screen.
+     *
+     * @return Returns true if one photo or video is displayed on the screen,
+     *         false otherwise.
+     */
+    private boolean isOnPhotoViewingScreen() {
+        return mDevice.hasObject(By.res(UI_PACKAGE_NAME, UI_PHOTO_VIEW_PAGER_ID));
+    }
+
+    private boolean isOnDeviceFolderScreen() {
+
+        if (mDevice.hasObject(By.pkg(UI_PACKAGE_NAME).text(UI_DEVICE_FOLDER_TEXT))) {
+            return true;
+        }
+
+        // sometimes the "Device Folder" tab is hidden.
+        // scroll down once to make sure the tab is visible
+        UiObject2 scrollContainer = mDevice.findObject(
+                                        By.res(UI_PACKAGE_NAME, UI_PHOTO_SCROLL_VIEW_ID));
+        if (scrollContainer != null) {
+            scrollContainer.scroll(Direction.DOWN, 1.0f);
+            return mDevice.hasObject(By.pkg(UI_PACKAGE_NAME).text(UI_DEVICE_FOLDER_TEXT));
+        }
+        else {
+            return false;
+        }
+    }
+
+    /**
+     * This function performs one scroll on the current screen, in the direction
+     * specified by input argument.
+     *
+     * @param dir The direction of the scroll
+     * @return Returns whether the object can still scroll in the given direction
+     */
+   private boolean scrollView(Direction dir) {
+        UiObject2 scrollContainer = mDevice.findObject(By.res(UI_PACKAGE_NAME,
+                                                              UI_PHOTO_SCROLL_VIEW_ID));
+        if (scrollContainer == null) {
+            return false;
+        }
+
+        return scrollContainer.scroll(dir, 1.0f);
+    }
+
+    private void openNavigationDrawer() {
+        UiObject2 navigationDrawer = mDevice.findObject(By.desc("Show Navigation Drawer"));
+        if (navigationDrawer == null) {
+            mDevice.pressBack();
+            navigationDrawer = mDevice.wait(Until.findObject(By.desc("Show Navigation Drawer")),
+                                            UI_NAVIGATION_WAIT);
+        }
+
+        if (navigationDrawer == null) {
+            throw new UnknownUiException("Cannot find navigation drawer");
+        }
+
+        navigationDrawer.click();
+
+        if (!mDevice.hasObject(By.res(UI_PACKAGE_NAME, UI_NAVIGATION_LIST_ID))) {
+            throw new UnknownUiException("Cannot open navigation drawer");
+        }
+    }
+}
diff --git a/libraries/play-books-app-helper/Android.mk b/libraries/play-books-app-helper/Android.mk
new file mode 100644
index 0000000..47da20e
--- /dev/null
+++ b/libraries/play-books-app-helper/Android.mk
@@ -0,0 +1,24 @@
+#
+# 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.
+#
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := play-books-app-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator base-app-helpers
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
diff --git a/libraries/play-books-app-helper/src/android/platform/test/helpers/PlayBooksHelperImpl.java b/libraries/play-books-app-helper/src/android/platform/test/helpers/PlayBooksHelperImpl.java
new file mode 100644
index 0000000..e54985a
--- /dev/null
+++ b/libraries/play-books-app-helper/src/android/platform/test/helpers/PlayBooksHelperImpl.java
@@ -0,0 +1,280 @@
+/*
+ * 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.os.SystemClock;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.Until;
+import android.support.test.uiautomator.UiObject2;
+
+import junit.framework.Assert;
+
+import java.lang.IllegalStateException;
+
+/**
+ * UI test helper for Play Books: The Official App (package: com.google.android.apps.books).
+ * Implementation based on app version: 3.8.15
+ */
+
+public class PlayBooksHelperImpl extends AbstractPlayBooksHelper {
+
+    private static final String LOG_TAG = PlayBooksHelperImpl.class.getSimpleName();
+
+    private static final String UI_PACKAGE_NAME = "com.google.android.apps.books";
+    private static final String UI_NAVIGATE_UP_DESC = "Navigate up";
+    private static final String UI_NAVIGATION_DRAWER_BUTTON_DESC = "Show navigation drawer";
+    private static final String UI_EXIT_BOOK_DESC = "Exit book";
+    private static final String UI_TAB_ALL_BOOKS_TEXT = "ALL BOOKS";
+    private static final String UI_NAVIGATION_DRAWER_SETTING_TEXT = "Settings";
+    private static final String UI_NAVIGATION_DRAWER_MYLIBRARY_TEXT = "My library";
+    private static final String UI_OPTION_MENU_READ_ALOUD_TEXT = "Read aloud";
+    private static final String UI_TURN_SYNC_ON_TEXT = "TURN SYNC ON";
+    private static final String UI_SKIP_TEXT = "SKIP";
+    private static final String UI_FULL_SCREEN_READER = "reader";
+    private static final String UI_PLAY_DRAWER_ROOT = "play_drawer_root";
+    private static final String UI_BOOK_THUMBNAIL = "li_thumbnail";
+
+    private static final long SKIP_DELAY = 2000; // 2 secs
+    private static final long UI_ANIMATION_TIMEOUT = 2500; // 2.5 secs
+    private static final long OPEN_BOOK_TIMEOUT = 10000; // 10 secs
+    private static final long SYNCING_BOOKS_TIMEOUT = 10000; //10 secs
+
+    public PlayBooksHelperImpl(Instrumentation instr) {
+        super(instr);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getPackage() {
+        return "com.google.android.apps.books";
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getLauncherName() {
+        return "Play Books";
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void dismissInitialDialogs() {
+        UiObject2 skipButton = getSkipButton();
+        if (skipButton != null) {
+            skipButton.click();
+            SystemClock.sleep(SKIP_DELAY);
+        }
+        UiObject2 turnSyncOnButton = getTurnSyncOnButton();
+        if (turnSyncOnButton != null) {
+            turnSyncOnButton.click();
+            SystemClock.sleep(SYNCING_BOOKS_TIMEOUT);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void goToAllBooksTab() {
+        closeOptionMenu();
+        exitReadingMode();
+        closeSettingPanel();
+        openNavigationDrawer();
+        UiObject2 myLibraryButton = getMyLibraryButton();
+        Assert.assertNotNull("Can't find \"My Library\" button", myLibraryButton);
+        myLibraryButton.click();
+        UiObject2 allBooksButton = mDevice.wait(Until.findObject(
+                By.text(UI_TAB_ALL_BOOKS_TEXT).clickable(true)),
+                UI_ANIMATION_TIMEOUT);
+        Assert.assertNotNull("Can't find \"ALL BOOKS\" tab button", allBooksButton);
+        allBooksButton.click();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void openBook() {
+        UiObject2 bookThumbNail = getBookThumbnail();
+        Assert.assertNotNull("No book in \"ALL BOOKS\" library", bookThumbNail);
+        bookThumbNail.click();
+        mDevice.wait(Until.hasObject(
+                By.res(UI_PACKAGE_NAME, UI_FULL_SCREEN_READER)),
+                OPEN_BOOK_TIMEOUT);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void exitReadingMode() {
+        UiObject2 exitBookButton = null;
+        UiObject2 fullScreenReader = getFullScreenReader();
+        if (fullScreenReader != null) {
+            fullScreenReader.click();
+            exitBookButton = mDevice.wait(
+                    Until.findObject(By.desc(UI_EXIT_BOOK_DESC)),
+                    UI_ANIMATION_TIMEOUT);
+            Assert.assertNotNull("Fail to exit full screen reader mode", exitBookButton);
+        } else {
+            exitBookButton = getExitBookButton();
+        }
+        if (exitBookButton != null) {
+            exitBookButton.click();
+            boolean hasNavButton = mDevice.wait(Until.hasObject(
+                    By.desc(UI_NAVIGATION_DRAWER_BUTTON_DESC)),
+                    UI_ANIMATION_TIMEOUT);
+            Assert.assertTrue("Fail to exit reading mode", hasNavButton);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void goToNextPage() {
+        UiObject2 fullScreenReader = getFullScreenReader();
+        if (fullScreenReader == null) {
+            throw new IllegalStateException("Not on a full-screen page of a book");
+        }
+        int displayHeight = mDevice.getDisplayHeight();
+        int displayWidth = mDevice.getDisplayWidth();
+        int nextPageX = displayWidth - 1;
+        int nextPageY = displayHeight / 2;
+        mDevice.click(nextPageX, nextPageY);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void goToPreviousPage() {
+        UiObject2 fullScreenReader = getFullScreenReader();
+        if (fullScreenReader == null) {
+            throw new IllegalStateException("Not on a full-screen page of a book");
+        }
+        int displayHeight = mDevice.getDisplayHeight();
+        int displayWidth = mDevice.getDisplayWidth();
+        int previousPageX = 0;
+        int previousPageY = displayHeight / 2;
+        mDevice.click(previousPageX, previousPageY);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void scrollToNextPage() {
+        UiObject2 fullScreenReader = getFullScreenReader();
+        if (fullScreenReader == null) {
+            throw new IllegalStateException("Not on a full-screen page of a book");
+        }
+        fullScreenReader.scroll(Direction.RIGHT, 1.0f);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void scrollToPreviousPage() {
+        UiObject2 fullScreenReader = getFullScreenReader();
+        if (fullScreenReader == null) {
+            throw new IllegalStateException("Not on a full-screen page of a book");
+        }
+        fullScreenReader.scroll(Direction.LEFT, 1.0f);
+    }
+
+    private void closeOptionMenu() {
+        if (isOptionMenuExpanded()) {
+            mDevice.pressBack();
+        }
+    }
+
+    private void closeSettingPanel() {
+        UiObject2 backButton = getBackButton();
+        if (backButton != null) {
+            backButton.click();
+            boolean hasNavButton = mDevice.wait(Until.hasObject(
+                    By.desc(UI_NAVIGATION_DRAWER_BUTTON_DESC)),
+                    UI_ANIMATION_TIMEOUT);
+            Assert.assertNotNull("Fail to close setting panel", hasNavButton);
+        }
+    }
+
+    private void openNavigationDrawer() {
+        if (isDrawerOpen()) {
+            return;
+        }
+        UiObject2 navButton = getNavButton();
+        Assert.assertNotNull("Unable to find navigation drawer button", navButton);
+        navButton.click();
+        waitForNavigationDrawerOpen();
+    }
+
+    private boolean isOptionMenuExpanded() {
+        return mDevice.hasObject(By.text(UI_OPTION_MENU_READ_ALOUD_TEXT));
+    }
+
+    private boolean isDrawerOpen() {
+        return mDevice.hasObject(By.res(UI_PACKAGE_NAME, UI_PLAY_DRAWER_ROOT));
+    }
+
+    private UiObject2 getSkipButton() {
+        return mDevice.findObject(By.text(UI_SKIP_TEXT));
+    }
+
+    private UiObject2 getTurnSyncOnButton() {
+        return mDevice.findObject(By.text(UI_TURN_SYNC_ON_TEXT));
+    }
+
+    private UiObject2 getFullScreenReader() {
+        return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_FULL_SCREEN_READER));
+    }
+
+    private UiObject2 getExitBookButton() {
+        return mDevice.findObject(By.desc(UI_EXIT_BOOK_DESC));
+    }
+
+    private UiObject2 getBackButton() {
+        return mDevice.findObject(By.desc(UI_NAVIGATE_UP_DESC));
+    }
+
+    private UiObject2 getNavButton() {
+        return mDevice.findObject(By.desc(UI_NAVIGATION_DRAWER_BUTTON_DESC));
+    }
+
+    private UiObject2 getMyLibraryButton() {
+        return mDevice.findObject(By.text(UI_NAVIGATION_DRAWER_MYLIBRARY_TEXT).clickable(true));
+    }
+
+    private UiObject2 getBookThumbnail() {
+        return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_BOOK_THUMBNAIL));
+    }
+
+    private void waitForNavigationDrawerOpen() {
+        mDevice.wait(Until.hasObject(
+                By.text(UI_NAVIGATION_DRAWER_SETTING_TEXT).clickable(true)),
+                UI_ANIMATION_TIMEOUT);
+    }
+}
\ No newline at end of file
diff --git a/libraries/play-movies-app-helper/Android.mk b/libraries/play-movies-app-helper/Android.mk
new file mode 100644
index 0000000..9c39a62
--- /dev/null
+++ b/libraries/play-movies-app-helper/Android.mk
@@ -0,0 +1,24 @@
+#
+# 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.
+#
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := play-movies-app-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator base-app-helpers
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
diff --git a/libraries/play-movies-app-helper/src/android/platform/test/helpers/PlayMoviesHelperImpl.java b/libraries/play-movies-app-helper/src/android/platform/test/helpers/PlayMoviesHelperImpl.java
new file mode 100644
index 0000000..99830f5
--- /dev/null
+++ b/libraries/play-movies-app-helper/src/android/platform/test/helpers/PlayMoviesHelperImpl.java
@@ -0,0 +1,215 @@
+/*
+ * 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.pm.PackageManager.NameNotFoundException;
+import android.os.SystemClock;
+import android.platform.test.helpers.exceptions.UnknownUiException;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Configurator;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.Until;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.util.Log;
+import android.widget.EditText;
+
+import java.util.regex.Pattern;
+
+public class PlayMoviesHelperImpl extends AbstractPlayMoviesHelper {
+    private static final String LOG_TAG = PlayMoviesHelperImpl.class.getSimpleName();
+
+    private static final String UI_PACKAGE = "com.google.android.videos";
+    private static final String UI_NAV_DRAWER_ID = "play_drawer_list";
+    private static final String UI_MOVIE_LIST_ID = "play_header_listview";
+
+    private static final int SEARCH_MOVIES_SCROLL_RETRY = 4;
+    private static final long APP_INIT_WAIT = 5000;
+
+    private boolean mIsVersion3p8 = false;
+
+    public PlayMoviesHelperImpl(Instrumentation instr) {
+        super(instr);
+
+        try {
+            mIsVersion3p8 = getVersion().startsWith("3.8");
+        } catch (NameNotFoundException e) {
+            Log.e(LOG_TAG, String.format("Unable to find package by name, %s", getPackage()));
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void open() {
+        long original = Configurator.getInstance().getWaitForIdleTimeout();
+        Configurator.getInstance().setWaitForIdleTimeout(1500);
+
+        super.open();
+
+        Configurator.getInstance().setWaitForIdleTimeout(original);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getPackage() {
+        return UI_PACKAGE;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getLauncherName() {
+        return "Play Movies & TV";
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void dismissInitialDialogs() {
+        if (mIsVersion3p8) {
+            BySelector nextButton = By.res(UI_PACKAGE, "end_button");
+            int count = 0;
+            while (mDevice.hasObject(nextButton) && count < 10) {
+                mDevice.findObject(nextButton).click();
+                mDevice.wait(Until.gone(nextButton), 1000);
+                count += 1;
+            }
+            BySelector gotIt = By.textContains("Got It");
+            count = 0;
+            while (mDevice.hasObject(gotIt) && count < 3) {
+                UiObject2 gotItButton = mDevice.findObject(gotIt);
+                if (gotItButton != null) {
+                    gotItButton.click();
+                    mDevice.wait(Until.gone(gotIt), 1000);
+                }
+                count += 1;
+            }
+        } else {
+            long original = Configurator.getInstance().getWaitForIdleTimeout();
+            Configurator.getInstance().setWaitForIdleTimeout(1500);
+
+            for (int retry = 0; retry < 5; retry++) {
+                Pattern words = Pattern.compile("GET STARTED", Pattern.CASE_INSENSITIVE);
+                UiObject2 startedButton = mDevice.wait(Until.findObject(By.text(words)), 5000);
+                if (startedButton != null) {
+                    startedButton.click();
+                }
+            }
+
+            Configurator.getInstance().setWaitForIdleTimeout(original);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void openMoviesTab() {
+        // Navigate to the Movies tab through the Navigation drawer
+        openNavigationDrawer();
+        Pattern myLibraryPattern = Pattern.compile("My Library", Pattern.CASE_INSENSITIVE);
+        UiObject2 libraryButton = mDevice.findObject(By.text(myLibraryPattern).clickable(true));
+        libraryButton.click();
+        waitForNavigationDrawerClose();
+        // Select the Movies tab if necessary
+        UiObject2 moviesTab = getMoviesTab();
+        if (moviesTab == null) {
+            throw new UnknownUiException("Unable to find the movies tab.");
+        }
+        if (!moviesTab.isSelected()) {
+            moviesTab.click();
+            mDevice.waitForIdle();
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void playMovie(String name) {
+        UiObject2 title = null;
+        for (int retry = 0; retry < SEARCH_MOVIES_SCROLL_RETRY; retry++) {
+            title = mDevice.findObject(By.textContains(name));
+            if (title == null) {
+                UiObject2 scroller = mDevice.findObject(By.res(UI_PACKAGE, UI_MOVIE_LIST_ID));
+                if (scroller != null) {
+                    scroller.scroll(Direction.DOWN, 1.0f);
+                }
+            }
+        }
+        if (title == null) {
+            throw new IllegalArgumentException(
+                    String.format("Failed to find movie by name %s", name));
+        }
+        title.click();
+        UiObject2 play = mDevice.wait(Until.findObject(By.res(UI_PACKAGE, "play")), 5000);
+        if (play == null) {
+            throw new UnknownUiException("Failed to find the play button.");
+        }
+        play.click();
+        mDevice.waitForIdle();
+    }
+
+    private boolean isNavigationDrawerOpen () {
+        return mDevice.hasObject(By.res(UI_PACKAGE, UI_NAV_DRAWER_ID));
+    }
+
+    private void openNavigationDrawer() {
+        if (isNavigationDrawerOpen()) {
+            return;
+        }
+
+        UiObject2 backButton = mDevice.findObject(By.pkg(getPackage()).desc("Navigate up"));
+        if (backButton != null) {
+            backButton.click();
+            mDevice.wait(Until.findObject(By.desc("Show navigation drawer")), 5000);
+        }
+
+        UiObject2 navButton = mDevice.findObject(By.desc("Show navigation drawer"));
+        if (navButton == null) {
+            throw new UnknownUiException("Unable to find the navigation drawer button.");
+        }
+        navButton.click();
+        waitForNavigationDrawerOpen();
+    }
+
+    private void waitForNavigationDrawerOpen() {
+        mDevice.wait(Until.hasObject(By.text("Settings").clickable(true)), 2500);
+    }
+
+    private void waitForNavigationDrawerClose() {
+        mDevice.wait(Until.gone(By.text("Settings").clickable(true)), 2500);
+    }
+
+    private UiObject2 getMoviesTab() {
+        Pattern moviesText = Pattern.compile("MY MOVIES", Pattern.CASE_INSENSITIVE);
+        UiObject2 tab = mDevice.findObject(By.text(moviesText));
+        if (tab == null) {
+            moviesText = Pattern.compile("MOVIES", Pattern.CASE_INSENSITIVE);
+            tab = mDevice.findObject(By.text(moviesText));
+        }
+        return tab;
+    }
+}
diff --git a/libraries/play-music-app-helper/Android.mk b/libraries/play-music-app-helper/Android.mk
new file mode 100644
index 0000000..b17b528
--- /dev/null
+++ b/libraries/play-music-app-helper/Android.mk
@@ -0,0 +1,23 @@
+#
+# 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.
+#
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := play-music-app-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator base-app-helpers
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/play-music-app-helper/src/android/platform/test/helpers/PlayMusicHelperImpl.java b/libraries/play-music-app-helper/src/android/platform/test/helpers/PlayMusicHelperImpl.java
new file mode 100644
index 0000000..b051ff1
--- /dev/null
+++ b/libraries/play-music-app-helper/src/android/platform/test/helpers/PlayMusicHelperImpl.java
@@ -0,0 +1,273 @@
+/*
+ * 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.pm.PackageManager.NameNotFoundException;
+import android.os.SystemClock;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.Until;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.util.Log;
+import android.widget.Button;
+import android.widget.ImageButton;
+import android.widget.TextView;
+
+import java.util.regex.Pattern;
+
+import junit.framework.Assert;
+
+public class PlayMusicHelperImpl extends AbstractPlayMusicHelper {
+    private static final String LOG_TAG = PlayMusicHelperImpl.class.getSimpleName();
+    private static final String UI_PACKAGE = "com.google.android.music";
+
+    private static final String UI_TAB_HEADER_ID = "play_header_list_tab_scroll";
+    private static final String UI_PAUSE_PLAY_BUTTON_ID = "play_pause_header";
+
+    private static final long APP_LOAD_WAIT = 10000;
+    private static final long APP_INIT_WAIT = 10000;
+    private static final long TAB_TRANSITION_WAIT = 5000;
+    private static final long EXPAND_WAIT = 5000;
+    private static final long NAV_BAR_WAIT = 5000;
+    private static final long TOGGLE_PAUSE_PLAY_WAIT = 5000;
+
+    public PlayMusicHelperImpl(Instrumentation instr) {
+        super(instr);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getPackage() {
+        return "com.google.android.music";
+    }
+
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getLauncherName() {
+        return "Play Music";
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void dismissInitialDialogs() {
+        // Dismiss "Add account" Dialog
+        UiObject2 skipButton = mDevice.wait(Until.findObject(By.res(UI_PACKAGE, "skip_button")),
+                APP_LOAD_WAIT);
+        if (skipButton != null) {
+            skipButton.clickAndWait(Until.newWindow(), APP_INIT_WAIT);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void goToTab(String tabTitle) {
+        if (isLibraryTabSelected(tabTitle)) {
+            return;
+        } else {
+            // Go to the "Library" page
+            goToMyLibrary();
+
+            for (int retries = 3; retries > 0; retries--) {
+                UiObject2 title = getLibraryTab(tabTitle);
+                if (title != null) {
+                    title.click();
+                    Assert.assertTrue(
+                            String.format("Tab %s was not found selected", tabTitle.toUpperCase()),
+                            mDevice.wait(
+                                    Until.hasObject(getLibraryTabSelector(tabTitle).selected(true)),
+                                    TAB_TRANSITION_WAIT));
+                } else {
+                    UiObject2 headerList = mDevice.findObject(By.res(UI_PACKAGE, UI_TAB_HEADER_ID));
+                    Assert.assertNotNull("Could not find library header to scroll.", headerList);
+                    headerList.scroll(Direction.RIGHT, 1.0f);
+                }
+            }
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void selectSong(String album, String song) {
+        UiObject2 albumItem = mDevice.wait(Until.findObject(By.res(UI_PACKAGE, "li_title")
+                .textStartsWith(album)), EXPAND_WAIT);
+        Assert.assertNotNull("Unable to find album item", albumItem);
+        albumItem.click();
+
+        mDevice.wait(Until.findObject(By.res(UI_PACKAGE, "title").textStartsWith(album)),
+                EXPAND_WAIT);
+
+        for (int retries = 5; retries > 0; retries--) {
+            UiObject2 songItem = mDevice.findObject(By.res(UI_PACKAGE, "li_title").
+                    textStartsWith(song));
+            if (songItem != null) {
+                songItem.click();
+                mDevice.wait(Until.findObject(
+                        By.res(UI_PACKAGE, "trackname").textStartsWith(song)), EXPAND_WAIT);
+
+                // Waits for the animation to complete.
+                mDevice.waitForIdle();
+                return;
+            } else {
+                UiObject2 scroller = mDevice.findObject(
+                        By.scrollable(true));
+                scroller.setGestureMargin(500);
+                scroller.scroll(Direction.DOWN, 1.0f);
+            }
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void pauseSong() {
+        BySelector selector1play = By.res(UI_PACKAGE, UI_PAUSE_PLAY_BUTTON_ID).desc("Play");
+        BySelector selector1pause = By.res(UI_PACKAGE, UI_PAUSE_PLAY_BUTTON_ID).desc("Pause");
+        BySelector selector2play = By.res(UI_PACKAGE, "pause").desc("Play");
+        BySelector selector2pause = By.res(UI_PACKAGE, "pause").desc("Pause");
+
+        UiObject2 button = null;
+        if ((button = mDevice.findObject(selector1play)) != null) {
+            return;
+        } else if ((button = mDevice.findObject(selector1pause)) != null) {
+            button.click();
+            mDevice.wait(Until.findObject(selector1play), TOGGLE_PAUSE_PLAY_WAIT);
+        } else if ((button = mDevice.findObject(selector2play)) != null) {
+            return;
+        } else if ((button = mDevice.findObject(selector2pause)) != null) {
+            button.click();
+            mDevice.wait(Until.findObject(selector2play), TOGGLE_PAUSE_PLAY_WAIT);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void playSong() {
+        BySelector selector1play = By.res(UI_PACKAGE, UI_PAUSE_PLAY_BUTTON_ID).desc("Play");
+        BySelector selector1pause = By.res(UI_PACKAGE, UI_PAUSE_PLAY_BUTTON_ID).desc("Pause");
+        BySelector selector2play = By.res(UI_PACKAGE, "pause").desc("Play");
+        BySelector selector2pause = By.res(UI_PACKAGE, "pause").desc("Pause");
+
+        UiObject2 button = null;
+        if ((button = mDevice.findObject(selector1pause)) != null) {
+            return;
+        } else if ((button = mDevice.findObject(selector1play)) != null) {
+            button.click();
+            mDevice.wait(Until.findObject(selector1pause), TOGGLE_PAUSE_PLAY_WAIT);
+        } else if ((button = mDevice.findObject(selector2pause)) != null) {
+            return;
+        } else if ((button = mDevice.findObject(selector2play)) != null) {
+            button.click();
+            mDevice.wait(Until.findObject(selector2pause), TOGGLE_PAUSE_PLAY_WAIT);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void expandMediaControls() {
+        UiObject2 header = mDevice.findObject(By.res(UI_PACKAGE, "trackname"));
+        Assert.assertNotNull("Unable to find header to expand media controls.", header);
+        header.click();
+        mDevice.wait(Until.findObject(By.res(UI_PACKAGE, "lightsUpInterceptor")), EXPAND_WAIT);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void pressShuffleAll() {
+        if (!isLibraryTabSelected("Songs")) {
+            throw new IllegalStateException("The Songs tab was not selected");
+        }
+
+        UiObject2 shuffleAll = mDevice.findObject(By.text("SHUFFLE ALL"));
+        Assert.assertNotNull("Could not find a 'SHUFFLE ALL' button.", shuffleAll);
+        shuffleAll.click();
+        Assert.assertTrue("Did not detect a song playing", mDevice.wait(Until.hasObject(
+            By.res(UI_PACKAGE, UI_PAUSE_PLAY_BUTTON_ID)), TOGGLE_PAUSE_PLAY_WAIT));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void pressRepeat() {
+        UiObject2 repeatButton = mDevice.findObject(By.res(UI_PACKAGE, "repeat"));
+        Assert.assertNotNull("Unable to find repeat button to press.", repeatButton);
+        repeatButton.click();
+        mDevice.waitForIdle();
+    }
+
+    private void goToMyLibrary() {
+        // Select for the title: "Library"
+        if (mDevice.findObject(getLibraryTextSelector().clickable(false)) != null) {
+            return;
+        }
+
+        openNavigationBar();
+        // Select for the button: "Library"
+        mDevice.findObject(getLibraryTextSelector().clickable(true)).click();
+        mDevice.wait(Until.gone(By.res(UI_PACKAGE, "play_drawer_root")), NAV_BAR_WAIT);
+    }
+
+    private void openNavigationBar () {
+        UiObject2 navBar = getNavigationBarButton();
+        Assert.assertNotNull("Did not find navigation drawer button.", navBar);
+        navBar.click();
+        mDevice.wait(Until.findObject(By.res(UI_PACKAGE, "play_drawer_root")), NAV_BAR_WAIT);
+    }
+
+    private UiObject2 getNavigationBarButton() {
+        return mDevice.findObject(By.desc("Show navigation drawer"));
+    }
+
+    private boolean isLibraryTabSelected(String tabTitle) {
+        return mDevice.hasObject(getLibraryTabSelector(tabTitle).selected(true));
+    }
+
+    private UiObject2 getLibraryTab(String tabTitle) {
+        return mDevice.findObject(getLibraryTabSelector(tabTitle));
+    }
+
+    private BySelector getLibraryTabSelector(String tabTitle) {
+        return By.res(UI_PACKAGE, "title").text(tabTitle.toUpperCase());
+    }
+
+    private BySelector getLibraryTextSelector() {
+        String libraryText = "Music library";
+        Pattern libraryTextPattern = Pattern.compile(libraryText, Pattern.CASE_INSENSITIVE);
+        return By.text(libraryTextPattern);
+    }
+}
diff --git a/libraries/play-store-app-helper/Android.mk b/libraries/play-store-app-helper/Android.mk
new file mode 100644
index 0000000..0a18e48
--- /dev/null
+++ b/libraries/play-store-app-helper/Android.mk
@@ -0,0 +1,23 @@
+#
+# 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.
+#
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := play-store-app-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator base-app-helpers
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/play-store-app-helper/src/android/platform/test/helpers/PlayStoreHelperImpl.java b/libraries/play-store-app-helper/src/android/platform/test/helpers/PlayStoreHelperImpl.java
new file mode 100644
index 0000000..2fbbcc5
--- /dev/null
+++ b/libraries/play-store-app-helper/src/android/platform/test/helpers/PlayStoreHelperImpl.java
@@ -0,0 +1,154 @@
+/*
+ * 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.pm.PackageManager.NameNotFoundException;
+import android.os.SystemClock;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.Until;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.util.Log;
+import android.widget.EditText;
+
+import junit.framework.Assert;
+
+public class PlayStoreHelperImpl extends AbstractPlayStoreHelper {
+    private static final String LOG_TAG = PlayStoreHelperImpl.class.getSimpleName();
+    private static final String UI_PACKAGE = "com.android.vending";
+
+    public PlayStoreHelperImpl(Instrumentation instr) {
+        super(instr);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getPackage() {
+        return "com.android.vending";
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getLauncherName() {
+        return "Play Store";
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void dismissInitialDialogs() {
+        UiObject2 tos = mDevice.findObject(By.res(UI_PACKAGE, "positive_button"));
+        if (tos != null) {
+            tos.clickAndWait(Until.newWindow(), 5000);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void doSearch(String query) {
+        // Back up and scroll up until search is visible
+        for (int retries = 3; retries > 0; retries--) {
+            if (getSearchBox() != null) {
+                break;
+            } else {
+                UiObject2 scroller = getScrollContainer();
+                if (scroller != null) {
+                    scroller.scroll(Direction.UP, 100.0f);
+                } else {
+                    mDevice.pressBack();
+                }
+            }
+        }
+
+        //Interact with the search box
+        UiObject2 searchBox = getSearchBox();
+        if (searchBox != null) {
+            searchBox.click();
+        } else {
+            Assert.fail("Unable to select search box.");
+        }
+        UiObject2 edit = mDevice.wait(
+                Until.findObject(By.clazz(EditText.class)), 5000);
+        Assert.assertNotNull("Could not find edit box", edit);
+        edit.setText(query);
+        mDevice.pressEnter();
+
+        // Wait until the search results container is open
+        Assert.assertTrue("Could not find search results",
+                mDevice.wait(Until.hasObject(By.res(UI_PACKAGE, "search_results_list")), 5000));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void selectFirstResult() {
+        try {
+            if (getVersion().startsWith("5.")) {
+                expandSection("Apps");
+            }
+        } catch (NameNotFoundException e) {
+            Log.e(LOG_TAG, "Unable to find version for package: " + UI_PACKAGE);
+        }
+        UiObject2 result = mDevice.findObject(By.res(UI_PACKAGE, "play_card"));
+        Assert.assertNotNull("Failed to find a result card", result);
+        result.click();
+    }
+
+    private void expandSection(String header) {
+        for (int retries = 3; retries > 0; retries--) {
+            BySelector section = By.res(UI_PACKAGE, "header_title_main").text(header);
+            UiObject2 title = mDevice.findObject(section);
+            if (title != null) {
+                title.click();
+                mDevice.wait(Until.gone(section), 5000);
+                return;
+            } else {
+                UiObject2 container = mDevice.findObject(By.res(UI_PACKAGE, "search_results_list"));
+                container.scroll(Direction.DOWN, 1.0f);
+            }
+        }
+        Assert.fail("Failed to find section header.");
+    }
+
+    private UiObject2 getSearchBox() {
+        UiObject2 searchBox = mDevice.findObject(By.res(UI_PACKAGE, "search_box_idle_text"));
+        if (searchBox == null) {
+            searchBox = mDevice.findObject(By.res(UI_PACKAGE, "search_button"));
+        }
+        return searchBox;
+    }
+
+    private UiObject2 getScrollContainer() {
+        UiObject2 scroller = mDevice.findObject(By.res(UI_PACKAGE, "recycler_view"));
+        if (scroller == null) {
+            scroller = mDevice.findObject(By.res(UI_PACKAGE, "viewpager"));
+        }
+        return scroller;
+    }
+}
+
diff --git a/libraries/recents-app-helper/Android.mk b/libraries/recents-app-helper/Android.mk
new file mode 100644
index 0000000..b565ad8
--- /dev/null
+++ b/libraries/recents-app-helper/Android.mk
@@ -0,0 +1,23 @@
+#
+# 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.
+#
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := recents-app-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator base-app-helpers
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/recents-app-helper/src/android/platform/test/helpers/RecentsHelperImpl.java b/libraries/recents-app-helper/src/android/platform/test/helpers/RecentsHelperImpl.java
new file mode 100644
index 0000000..58e39ae
--- /dev/null
+++ b/libraries/recents-app-helper/src/android/platform/test/helpers/RecentsHelperImpl.java
@@ -0,0 +1,102 @@
+/*
+ * 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.os.RemoteException;
+import android.os.SystemClock;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.util.Log;
+import android.widget.EditText;
+
+import junit.framework.Assert;
+
+public class RecentsHelperImpl extends AbstractRecentsHelper {
+    private static final String LOG_TAG = RecentsHelperImpl.class.getSimpleName();
+    private static final String UI_PACKAGE = "com.android.systemui";
+
+    private static final long RECENTS_SELECTION_TIMEOUT = 5000;
+
+    public RecentsHelperImpl(Instrumentation instr) {
+        super(instr);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getPackage() {
+        throw new UnsupportedOperationException("This method is not supported for Recents");
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getLauncherName() {
+        throw new UnsupportedOperationException("This method is not supported for Recents");
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void open() {
+        try {
+            mDevice.pressRecentApps();
+            mDevice.waitForIdle();
+        } catch (RemoteException ex) {
+            Log.e(LOG_TAG, ex.toString());
+        }
+    }
+
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void exit() {
+        mDevice.pressHome();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void dismissInitialDialogs() {
+        // Nothing to do.
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void flingRecents(Direction dir) {
+        UiObject2 recentsScroller = getRecentsScroller();
+        Assert.assertNotNull("Unable to find scrolling mechanism for Recents", recentsScroller);
+        recentsScroller.setGestureMargin(recentsScroller.getVisibleBounds().height() / 4);
+        recentsScroller.fling(dir);
+    }
+
+    private UiObject2 getRecentsScroller() {
+        return mDevice.wait(Until.findObject(By.res(UI_PACKAGE, "recents_view")),
+                RECENTS_SELECTION_TIMEOUT);
+    }
+}
diff --git a/libraries/reddit-app-helper/Android.mk b/libraries/reddit-app-helper/Android.mk
new file mode 100644
index 0000000..1ad7af7
--- /dev/null
+++ b/libraries/reddit-app-helper/Android.mk
@@ -0,0 +1,23 @@
+#
+# 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.
+#
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := reddit-app-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator base-app-helpers
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/reddit-app-helper/src/android/platform/test/helpers/RedditHelperImpl.java b/libraries/reddit-app-helper/src/android/platform/test/helpers/RedditHelperImpl.java
new file mode 100644
index 0000000..d6e2784
--- /dev/null
+++ b/libraries/reddit-app-helper/src/android/platform/test/helpers/RedditHelperImpl.java
@@ -0,0 +1,143 @@
+/*
+ * 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.os.SystemClock;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.Until;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.util.Log;
+
+import junit.framework.Assert;
+
+/**
+ * UI test helper for Reddit: The Official App (package: com.reddit.frontpage)
+ */
+
+public class RedditHelperImpl extends AbstractRedditHelper {
+    private static final String TAG = RedditHelperImpl.class.getSimpleName();
+
+    private static final String UI_COMMENTS_PAGE_SCROLL_CONTAINER_ID = "detail_list";
+    private static final String UI_FRONT_PAGE_SCROLL_CONTAINER_ID = "link_list";
+    private static final String UI_LINK_TITLE_ID = "link_title";
+    private static final String UI_PACKAGE_NAME = "com.reddit.frontpage";
+    private static final String UI_REDDIT_WORDMARK_ID = "reddit_wordmark";
+    private static final String UI_SAVE_BUTTON_ID = "action_save";
+
+    private static final long UI_NAVIGATION_WAIT = 5000; // 5 secs
+
+    public RedditHelperImpl(Instrumentation instr) {
+        super(instr);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getPackage() {
+        return UI_PACKAGE_NAME;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getLauncherName() {
+        return "Reddit";
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void dismissInitialDialogs() {
+
+    }
+
+    private UiObject2 getRedditWordmark() {
+        return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_REDDIT_WORDMARK_ID));
+    }
+
+    private UiObject2 getFrontPageScrollContainer() {
+        return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_FRONT_PAGE_SCROLL_CONTAINER_ID));
+    }
+
+    private UiObject2 getFirstArticleTitle() {
+        return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_LINK_TITLE_ID));
+    }
+
+    private UiObject2 getSaveButton() {
+        return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_SAVE_BUTTON_ID));
+    }
+
+    private UiObject2 getCommentPageScrollContainer() {
+        return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_COMMENTS_PAGE_SCROLL_CONTAINER_ID));
+    }
+
+    private boolean isOnFrontPage() {
+        return (getRedditWordmark() != null);
+    }
+
+    private boolean isOnCommentsPage() {
+        return (getSaveButton() != null);
+    }
+
+    public void goToFrontPage() {
+        for (int retriesRemaining = 5; retriesRemaining > 0 && !isOnFrontPage();
+                --retriesRemaining) {
+            mDevice.pressBack();
+            mDevice.waitForIdle();
+        }
+    }
+
+    public void goToFirstArticleComments() {
+        Assert.assertTrue("Not on front page", isOnFrontPage());
+
+        UiObject2 articleTitle = getFirstArticleTitle();
+        Assert.assertNotNull("Could not find first article", articleTitle);
+
+        articleTitle.click();
+        mDevice.wait(Until.hasObject(By.res(UI_PACKAGE_NAME, UI_SAVE_BUTTON_ID)),
+                UI_NAVIGATION_WAIT);
+    }
+
+    public boolean scrollFrontPage(Direction direction, float percent) {
+        Assert.assertTrue("Not on front page", isOnFrontPage());
+        Assert.assertTrue("Scroll direction must be UP or DOWN",
+                Direction.UP.equals(direction) || Direction.DOWN.equals(direction));
+
+        UiObject2 scrollContainer = getFrontPageScrollContainer();
+        Assert.assertNotNull("Could not find front page scroll container", scrollContainer);
+
+        return scrollContainer.scroll(direction, percent);
+    }
+
+    public boolean scrollCommentPage(Direction direction, float percent) {
+        Assert.assertTrue("Not on comment page", isOnCommentsPage());
+        Assert.assertTrue("Scroll direction must be UP or DOWN",
+                Direction.UP.equals(direction) || Direction.DOWN.equals(direction));
+
+        UiObject2 scrollContainer = getCommentPageScrollContainer();
+        Assert.assertNotNull("Could not find comment page scroll container", scrollContainer);
+
+        return scrollContainer.scroll(direction, percent);
+    }
+}
diff --git a/libraries/settings-app-helper/Android.mk b/libraries/settings-app-helper/Android.mk
new file mode 100644
index 0000000..462e592
--- /dev/null
+++ b/libraries/settings-app-helper/Android.mk
@@ -0,0 +1,23 @@
+#
+# 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.
+#
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE := settings-app-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator base-app-helpers
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/settings-app-helper/src/android/platform/test/helpers/SettingsHelperImpl.java b/libraries/settings-app-helper/src/android/platform/test/helpers/SettingsHelperImpl.java
new file mode 100644
index 0000000..206c910
--- /dev/null
+++ b/libraries/settings-app-helper/src/android/platform/test/helpers/SettingsHelperImpl.java
@@ -0,0 +1,218 @@
+/*
+ * 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.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.provider.Settings;
+import android.provider.Settings.SettingNotFoundException;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.platform.test.helpers.AbstractSettingsHelper;
+import android.util.Log;
+
+import junit.framework.Assert;
+
+import java.util.regex.Pattern;
+
+public class SettingsHelperImpl extends AbstractSettingsHelper {
+
+    private static final int SETTINGS_DASH_TIMEOUT = 3000;
+    private static final String UI_PACKAGE_NAME = "com.android.settings";
+    private static final BySelector SETTINGS_DASHBOARD = By.res(UI_PACKAGE_NAME,
+            "dashboard_container");
+    private static final int TIMEOUT = 2000;
+    private static final String LOG_TAG = SettingsHelperImpl.class.getSimpleName();
+
+    private ContentResolver mResolver;
+
+    public static enum SettingsType {
+        SYSTEM,
+        SECURE,
+        GLOBAL
+    }
+
+    public SettingsHelperImpl(Instrumentation instr) {
+        super(instr);
+        mResolver = instr.getContext().getContentResolver();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getPackage() {
+        return "com.android.settings";
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getLauncherName() {
+        return "Settings";
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void dismissInitialDialogs() {
+    }
+
+     /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void scrollThroughSettings(int numberOfFlings) throws Exception {
+        UiObject2 settingsList = loadAllSettings();
+        int count = 0;
+        while (count <= numberOfFlings && settingsList.fling(Direction.DOWN)) {
+            count++;
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void flingSettingsToStart() throws Exception {
+        UiObject2 settingsList = loadAllSettings();
+        while (settingsList.fling(Direction.UP));
+    }
+
+    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);
+    }
+
+    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.
+     */
+    private UiObject2 loadAllSettings() throws Exception {
+        UiObject2 settingsDashboard = mDevice.wait(Until.findObject(SETTINGS_DASHBOARD),
+                SETTINGS_DASH_TIMEOUT);
+        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")), SETTINGS_DASH_TIMEOUT).click();
+            settingsDashboard = mDevice.wait(Until.findObject(SETTINGS_DASHBOARD),
+                    SETTINGS_DASH_TIMEOUT);
+            count++;
+        }
+        return settingsDashboard;
+    }
+
+    public void clickSetting(String settingName) throws InterruptedException {
+        mDevice.wait(Until.findObject(By.text(settingName)), TIMEOUT).click();
+        Thread.sleep(400);
+    }
+
+    public void clickSetting(Pattern settingName) throws InterruptedException {
+        mDevice.wait(Until.findObject(By.text(settingName)), TIMEOUT).click();
+        Thread.sleep(400);
+    }
+
+    public boolean verifyToggleSetting(SettingsType type, String settingAction,
+            String settingName, String internalName) throws Exception {
+        return verifyToggleSetting(
+                type, settingAction, Pattern.compile(settingName), internalName, true);
+    }
+
+    public boolean verifyToggleSetting(SettingsType type, String settingAction,
+            Pattern settingName, String internalName) throws Exception {
+        return verifyToggleSetting(type, settingAction, settingName, internalName, true);
+    }
+
+    public boolean verifyToggleSetting(SettingsType type, String settingAction,
+            String settingName, String internalName, boolean doLaunch) throws Exception {
+        return verifyToggleSetting(
+                type, settingAction, Pattern.compile(settingName), internalName, doLaunch);
+    }
+
+    public boolean verifyToggleSetting(SettingsType type, String settingAction,
+            Pattern settingName, String internalName, boolean doLaunch) throws Exception {
+        String onSettingBaseVal = getStringSetting(type, internalName);
+        if (onSettingBaseVal == null) {
+            onSettingBaseVal = "0";
+        }
+        int onSetting = Integer.parseInt(onSettingBaseVal);
+        Log.d(null, "On Setting value is : " + onSetting);
+        if (doLaunch) {
+            launchSettingsPage(mInstrumentation.getContext(), settingAction);
+        }
+        clickSetting(settingName);
+        Log.d(null, "Clicked setting : " + settingName);
+        Thread.sleep(1000);
+        String changedSetting = getStringSetting(type, internalName);
+        Log.d(null, "Changed Setting value is : " + changedSetting);
+        if (changedSetting == null) {
+            Log.d(null, "Changed Setting value is : NULL");
+            changedSetting = "0";
+        }
+        return (1 - onSetting) == Integer.parseInt(changedSetting);
+    }
+
+    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);
+    }
+
+    private 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 "";
+    }
+
+    private 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;
+    }
+}
diff --git a/libraries/tunein-app-helper/Android.mk b/libraries/tunein-app-helper/Android.mk
new file mode 100644
index 0000000..bf4c2a3
--- /dev/null
+++ b/libraries/tunein-app-helper/Android.mk
@@ -0,0 +1,23 @@
+#
+# 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.
+#
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := tunein-app-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator base-app-helpers
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/tunein-app-helper/src/android/platform/test/helpers/TuneInHelperImpl.java b/libraries/tunein-app-helper/src/android/platform/test/helpers/TuneInHelperImpl.java
new file mode 100644
index 0000000..f92476a
--- /dev/null
+++ b/libraries/tunein-app-helper/src/android/platform/test/helpers/TuneInHelperImpl.java
@@ -0,0 +1,194 @@
+/*
+ * 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.platform.test.helpers.exceptions.UnknownUiException;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Until;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.util.Log;
+import junit.framework.Assert;
+
+public class TuneInHelperImpl extends AbstractTuneInHelper {
+    private static final String TAG = TuneInHelperImpl.class.getCanonicalName();
+
+    private static final String UI_PACKAGE_NAME = "tunein.player";
+    private static final long UI_ACTION_TIMEOUT = 5000;
+    private static final int MAX_BACK_ATTEMPTS = 5;
+
+    private static final String UI_LOCAL_RADIO_TEXT = "Local Radio";
+    private static final String UI_FM_LIST_ID = "view_model_list";
+    private static final String UI_START_PLAY_ID = "profile_primary_button";
+    private static final String UI_MINI_PLAYER_PLAY_ID = "mini_player_play";
+    private static final String UI_MINI_PLAYER_STOP_ID = "mini_player_stop";
+
+    public TuneInHelperImpl(Instrumentation instr) {
+        super(instr);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getPackage() {
+        return UI_PACKAGE_NAME;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getLauncherName() {
+        return "TuneIn Radio";
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void dismissInitialDialogs() {
+
+    }
+
+    private boolean isOnBrowsePage() {
+        return mDevice.hasObject(By.text("Browse"));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void goToBrowsePage() {
+        for (int tries = MAX_BACK_ATTEMPTS; tries > 0; tries--) {
+            if (isOnBrowsePage()) {
+                break;
+            }
+            mDevice.pressBack();
+            mDevice.waitForIdle();
+        }
+        if (!isOnBrowsePage()) {
+            throw new IllegalStateException("Fail to go to Browse Page");
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void goToLocalRadio() {
+        if (!isOnBrowsePage()) {
+            throw new IllegalStateException("Not on Browse Page");
+        }
+
+        UiObject2 localRadio = mDevice.findObject(By.text(UI_LOCAL_RADIO_TEXT));
+
+        if (localRadio == null) {
+            throw new UnknownUiException("Cannot not find local radio");
+        }
+        else {
+            if (!localRadio.clickAndWait(Until.newWindow(), UI_ACTION_TIMEOUT)) {
+                throw new UnknownUiException("Fail to load Local Radio page");
+            }
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void selectFM(int i) {
+        UiObject2 fmList = mDevice.wait(
+              Until.findObject(By.res(UI_PACKAGE_NAME, UI_FM_LIST_ID)),
+              UI_ACTION_TIMEOUT
+            );
+
+        if (fmList == null) {
+            throw new UnknownUiException("Cannot not find fm list to select FM");
+        }
+
+        if (i <= 0 && i >= fmList.getChildren().size()) {
+            String errMsg = String.format("Trying to select %dth FM radio, valid range = (1, %d)",
+                                          i, fmList.getChildren().size() - 1);
+            throw new IllegalArgumentException(errMsg);
+        }
+
+        UiObject2 fm = fmList.getChildren().get(i);
+
+        if (!fm.clickAndWait(Until.newWindow(), UI_ACTION_TIMEOUT)) {
+            throw new UnknownUiException("Fail to load into fm profile page");
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void startChannel() {
+        if (isOnFeedbackScreen()) {
+            dismissFeedbackScreen();
+        }
+
+        UiObject2 start = mDevice
+            .findObject(By.res(UI_PACKAGE_NAME, UI_START_PLAY_ID));
+
+        if (start == null) {
+            throw new UnknownUiException("Cannot find start play button");
+        }
+
+        if (!start.clickAndWait(Until.newWindow(), UI_ACTION_TIMEOUT)) {
+            throw new UnknownUiException("Fail to start playing the fm");
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void stopChannel() {
+        if (isOnFeedbackScreen()) {
+            dismissFeedbackScreen();
+        }
+
+        UiObject2 stop = mDevice
+            .findObject(By.res(UI_PACKAGE_NAME, UI_MINI_PLAYER_STOP_ID));
+
+        if (stop == null) {
+            throw new UnknownUiException("Could not find stop button");
+        }
+
+        stop.click();
+
+        if (!stop.wait(Until.enabled(!stop.isEnabled()), UI_ACTION_TIMEOUT)) {
+            throw new UnknownUiException("Fail to stop playing the fm");
+        }
+    }
+
+    private boolean isOnFeedbackScreen() {
+        return mDevice.hasObject(By.text("Do you love TuneIn Radio?"));
+    }
+
+    private void dismissFeedbackScreen() {
+        UiObject2 button = mDevice.findObject(By.text("MAYBE LATER"));
+
+        if (button != null) {
+            button.click();
+        }
+    }
+
+}
diff --git a/libraries/youtube-app-helper/Android.mk b/libraries/youtube-app-helper/Android.mk
new file mode 100644
index 0000000..10fb921
--- /dev/null
+++ b/libraries/youtube-app-helper/Android.mk
@@ -0,0 +1,23 @@
+#
+# 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.
+#
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := youtube-app-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator base-app-helpers
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/youtube-app-helper/src/android/platform/test/helpers/YouTubeHelperImpl.java b/libraries/youtube-app-helper/src/android/platform/test/helpers/YouTubeHelperImpl.java
new file mode 100644
index 0000000..2750f47
--- /dev/null
+++ b/libraries/youtube-app-helper/src/android/platform/test/helpers/YouTubeHelperImpl.java
@@ -0,0 +1,493 @@
+/*
+ * 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.platform.test.helpers;
+
+import android.app.Instrumentation;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Configuration;
+import android.graphics.Point;
+import android.os.SystemClock;
+import android.platform.test.helpers.exceptions.UiTimeoutException;
+import android.platform.test.helpers.exceptions.UnknownUiException;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.Until;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.util.Log;
+
+import java.util.regex.Pattern;
+
+public class YouTubeHelperImpl extends AbstractYouTubeHelper {
+    private static final String TAG = AbstractYouTubeHelper.class.getSimpleName();
+
+    private static final String UI_ACCOUNT_BUTTON_DESC = "Account";
+    private static final String UI_HOME_CONTAINER_ID = "results";
+    private static final String UI_FULLSCREEN_BUTTON_DESC = "Enter fullscreen";
+    private static final String UI_HELP_AND_FEEDBACK_TEXT = "Help & feedback";
+    private static final String UI_HOME_BUTTON_DESC = "Home";
+    private static final String UI_HOME_PAGE_VIDEO_ID = "event_item";
+    private static final String UI_VIDEO_INFO_VIEW_ID = "video_info_view";
+    private static final String UI_PACKAGE_NAME = "com.google.android.youtube";
+    private static final String UI_PLAY_VIDEO_DESC = "Play video";
+    private static final String UI_PROGRESS_ID = "load_progress";
+    private static final String UI_RESULT_FILTER_ID = "menu_filter_results";
+    private static final String UI_SEARCH_BUTTON_ID = "menu_search";
+    private static final String UI_SEARCH_EDIT_TEXT_ID = "search_edit_text";
+    private static final String UI_SELECT_DIALOG_LISTVIEW_ID = "select_dialog_listview";
+    private static final String UI_TRENDING_BUTTON_DESC = "Trending";
+    private static final String UI_VIDEO_PLAYER_ID = "watch_player";
+    private static final String UI_VIDEO_PLAYER_OVERFLOW_BUTTON_ID = "player_overflow_button";
+    private static final String UI_VIDEO_PLAYER_PLAY_PAUSE_REPLAY_BUTTON_ID =
+            "player_control_play_pause_replay_button";
+    private static final String UI_VIDEO_PLAYER_QUALITY_BUTTON_ID = "quality_button";
+
+    private static final long MAX_HOME_LOAD_WAIT = 30 * 1000;
+    private static final long MAX_VIDEO_LOAD_WAIT = 30 * 1000;
+
+    private static final long APP_INIT_WAIT = 20000;
+    private static final long STANDARD_DIALOG_WAIT = 5000;
+    private static final long UI_NAVIGATION_WAIT = 5000;
+
+    public YouTubeHelperImpl(Instrumentation instr) {
+        super(instr);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getPackage() {
+        return "com.google.android.youtube";
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getLauncherName() {
+        return "YouTube";
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void dismissInitialDialogs() {
+        BySelector dialog1 = By.text("OK");
+        // Dismiss the splash screen that might appear on first start.
+        UiObject2 splash = mDevice.wait(Until.findObject(dialog1), APP_INIT_WAIT);
+        if (splash != null) {
+            splash.click();
+            mDevice.wait(Until.gone(dialog1), STANDARD_DIALOG_WAIT);
+        }
+
+        UiObject2 laterButton = mDevice.wait(Until.findObject(
+                By.res(UI_PACKAGE_NAME, "later_button")), STANDARD_DIALOG_WAIT);
+        if (laterButton != null) {
+            laterButton.clickAndWait(Until.newWindow(), STANDARD_DIALOG_WAIT);
+        }
+
+        UiObject2 helpAndFeedbackButton = mDevice.findObject(
+            By.pkg(UI_PACKAGE_NAME).text(UI_HELP_AND_FEEDBACK_TEXT));
+        if (helpAndFeedbackButton != null) {
+            mDevice.pressBack();
+            mDevice.wait(Until.gone(By.pkg(UI_PACKAGE_NAME).text(UI_HELP_AND_FEEDBACK_TEXT)),
+                STANDARD_DIALOG_WAIT);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void playHomePageVideo() {
+        if (!isOnHomePage()) {
+            throw new IllegalStateException("YouTube is not on the home page.");
+        }
+
+        if (hasConnectionEstablishedMessage()) {
+            pressGoOnline();
+        }
+
+        for (int i = 0; i < 3; i++) {
+            UiObject2 video = getPlayableVideo();
+            if (video != null) {
+                video.click();
+                waitForVideoToLoad(UI_NAVIGATION_WAIT);
+                return;
+            } else {
+                scrollHomePage(Direction.DOWN);
+            }
+        }
+
+        if (isLoading()) {
+            throw new UiTimeoutException("Timed out waiting for video search results.");
+        }
+
+        throw new UnknownUiException("Unsuccessful attempt playing home page video.");
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void playSearchResultPageVideo() {
+        if (!isOnSearchResultsPage()) {
+            throw new IllegalStateException("YouTube is not on the home page.");
+        }
+
+        for (int i = 0; i < 3; i++) {
+            UiObject2 video = getPlayableVideo();
+            if (video != null) {
+                video.click();
+                waitForVideoToLoad(UI_NAVIGATION_WAIT);
+                return;
+            } else {
+                scrollSearchResultsPage(Direction.DOWN);
+            }
+        }
+
+        throw new UnknownUiException("Unsuccessful attempt playing search result video.");
+    }
+
+    private UiObject2 getHomePageContainer() {
+        return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_HOME_CONTAINER_ID));
+    }
+
+    private UiObject2 getSearchResultsPageContainer() {
+        return getHomePageContainer();
+    }
+
+    private UiObject2 getHomeButton() {
+        return mDevice.findObject(By.pkg(UI_PACKAGE_NAME).desc(UI_HOME_BUTTON_DESC));
+    }
+
+    private UiObject2 getTrendingButton() {
+        return mDevice.findObject(By.pkg(UI_PACKAGE_NAME).desc(UI_TRENDING_BUTTON_DESC));
+    }
+
+    private UiObject2 getAccountButton() {
+        return mDevice.findObject(By.pkg(UI_PACKAGE_NAME).desc(UI_ACCOUNT_BUTTON_DESC));
+    }
+
+    private UiObject2 getSearchButton() {
+        return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_SEARCH_BUTTON_ID));
+    }
+
+    private void scrollHomePage(Direction dir) {
+        if (dir == Direction.RIGHT || dir == Direction.LEFT) {
+            throw new IllegalArgumentException("Can only scroll up and down.");
+        }
+
+        UiObject2 scrollContainer = getHomePageContainer();
+        if (scrollContainer != null) {
+            scrollContainer.scroll(dir, 1.0f);
+            mDevice.waitForIdle();
+        } else {
+            throw new UnknownUiException("No scrolling mechanism found.");
+        }
+    }
+
+    private void scrollSearchResultsPage(Direction dir) {
+        if (dir == Direction.RIGHT || dir == Direction.LEFT) {
+            throw new IllegalArgumentException("Can only scroll up and down.");
+        }
+
+        UiObject2 scrollContainer = getSearchResultsPageContainer();
+        if (scrollContainer != null) {
+            scrollContainer.scroll(dir, 1.0f);
+            mDevice.waitForIdle();
+        } else {
+            throw new UnknownUiException("No scrolling mechanism found.");
+        }
+    }
+
+    private boolean isLoading() {
+        // TODO: Is loading what? Requires more documentation.
+        return mDevice.hasObject(By.res(UI_PACKAGE_NAME, UI_PROGRESS_ID));
+    }
+
+    private boolean isOnHomePage() {
+        UiObject2 homeButton = getHomeButton();
+        return (homeButton != null && homeButton.isSelected());
+    }
+
+    private boolean isOnTrendingPage() {
+        UiObject2 trendingButton = getTrendingButton();
+        return (trendingButton != null && trendingButton.isSelected());
+    }
+
+    private boolean isOnAccountPage() {
+        UiObject2 accountButton = getAccountButton();
+        return (accountButton != null && accountButton.isSelected());
+    }
+
+    private boolean isOnSearchResultsPage() {
+        // Simplest way to identify search result page is the result filter button.
+        UiObject2 resultFilterButton =
+                mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_RESULT_FILTER_ID));
+        return (resultFilterButton != null);
+    }
+
+    private UiObject2 getPlayableVideo() {
+        UiObject2 video = mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_HOME_PAGE_VIDEO_ID));
+        if (video == null) {
+            video = mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_VIDEO_INFO_VIEW_ID));
+        }
+        return video;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean waitForVideoToLoad(long timeout) {
+        return mDevice.wait(Until.hasObject(
+            By.res(UI_PACKAGE_NAME, UI_VIDEO_PLAYER_ID)), timeout);
+    }
+
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void goToHomePage() {
+        for (int retriesRemaining = 5; retriesRemaining > 0 && getHomeButton() == null &&
+                getTrendingButton() == null && getAccountButton() == null; --retriesRemaining) {
+            mDevice.pressBack();
+            SystemClock.sleep(3000);
+        }
+        // Get and press the home button
+        UiObject2 homeButton = getHomeButton();
+        if (homeButton == null) {
+            throw new UnknownUiException("Could not find home button.");
+        } else if (!homeButton.isSelected()) {
+            homeButton.click();
+            // Validate the home button is selected
+            if (!mDevice.wait(Until.hasObject(
+                    By.pkg(UI_PACKAGE_NAME).desc(UI_HOME_BUTTON_DESC).selected(true)),
+                    UI_NAVIGATION_WAIT)) {
+                throw new UnknownUiException("Not on home page after pressing home button.");
+            } else {
+                // Make sure the transition is complete
+                mDevice.waitForIdle();
+            }
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void goToSearchPage() {
+        if (!isOnHomePage()) {
+            throw new IllegalStateException("YouTube is not on the home page.");
+        }
+
+        UiObject2 searchButton = getSearchButton();
+        if (searchButton == null) {
+            throw new UnknownUiException("Could not find search button.");
+        } else {
+            searchButton.click();
+            if (!mDevice.wait(Until.hasObject(
+                    By.res(UI_PACKAGE_NAME, UI_SEARCH_EDIT_TEXT_ID)), UI_NAVIGATION_WAIT)) {
+                throw new UnknownUiException("Not on search page after pressing search button.");
+            }
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void goToFullscreenMode() {
+        if (!isOnVideo()) {
+            throw new IllegalStateException("YouTube is not on a video page.");
+        }
+
+        if (getOrientation() == Configuration.ORIENTATION_LANDSCAPE) {
+            return;
+        }
+
+        UiObject2 fullscreenButton = null;
+        for (int retriesRemaining = 5; retriesRemaining > 0; --retriesRemaining) {
+            UiObject2 miniVideoPlayer = getVideoPlayer();
+            if (miniVideoPlayer == null) {
+                throw new UnknownUiException("Could not find mini video player.");
+            }
+
+            miniVideoPlayer.click();
+            SystemClock.sleep(1500);
+            fullscreenButton = getFullscreenButton();
+            if (fullscreenButton != null) {
+                fullscreenButton.click();
+                // TODO: Add a valid wait for fullscreen
+                break;
+            }
+        }
+
+        if (fullscreenButton == null) {
+            throw new UnknownUiException("Did not find a fullscreen button.");
+        }
+    }
+
+    private UiObject2 getVideoPlayer() {
+        return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_VIDEO_PLAYER_ID));
+    }
+
+    private boolean isOnVideo() {
+        return (getVideoPlayer() != null);
+    }
+
+    private UiObject2 getVideoPlayerOverflowButton() {
+        return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_VIDEO_PLAYER_OVERFLOW_BUTTON_ID));
+    }
+
+    private UiObject2 getVideoPlayerQualityButton() {
+        UiObject2 videoPlayer = getVideoPlayer();
+        UiObject2 qualityButton = null;
+
+        if (videoPlayer != null) {
+            qualityButton = mDevice.findObject(
+                    By.res(UI_PACKAGE_NAME, UI_VIDEO_PLAYER_QUALITY_BUTTON_ID));
+        }
+        return qualityButton;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean waitForSearchResults(long timeout) {
+        return mDevice.wait(Until.hasObject(
+                By.res(UI_PACKAGE_NAME, UI_VIDEO_INFO_VIEW_ID)), timeout);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void setVideoQuality(VideoQuality quality) {
+        if (!isOnVideo()) {
+            throw new IllegalStateException("YouTube is not on a video page.");
+        }
+
+        UiObject2 overflowButton = getVideoPlayerOverflowButton();
+        // Open the mini video player
+        if (overflowButton == null) {
+            UiObject2 miniVideoPlayer = getVideoPlayer();
+            if (miniVideoPlayer == null) {
+                throw new UnknownUiException("Could not find mini video player.");
+            }
+
+            miniVideoPlayer.click();
+            mDevice.wait(Until.findObject(By.res(
+                UI_PACKAGE_NAME, UI_VIDEO_PLAYER_OVERFLOW_BUTTON_ID)), UI_NAVIGATION_WAIT);
+            overflowButton = getVideoPlayerOverflowButton();
+        }
+
+        if (overflowButton == null) {
+            throw new UnknownUiException("Could not find overflow button.");
+        }
+
+        overflowButton.click();
+        UiObject2 qualityButton = mDevice.wait(Until.findObject(
+                By.res(UI_PACKAGE_NAME, UI_VIDEO_PLAYER_QUALITY_BUTTON_ID)), UI_NAVIGATION_WAIT);
+        if (qualityButton == null) {
+            throw new UnknownUiException("Could not find video quality button.");
+        }
+
+        qualityButton.click();
+        UiObject2 quality360pLabel = mDevice.wait(Until.findObject(By.text(
+                AbstractYouTubeHelper.VideoQuality.QUALITY_360p.getText())), UI_NAVIGATION_WAIT);
+        if (quality360pLabel == null) {
+            throw new UnknownUiException("Could not find 360p quality label.");
+        }
+
+        UiObject2 selectDialog = quality360pLabel.getParent();
+        if (selectDialog == null) {
+            throw new UnknownUiException("Could not find video quality dialog.");
+        }
+
+        UiObject2 qualityLabel = null;
+        for (int retriesRemaining = 5; retriesRemaining > 0; --retriesRemaining) {
+            qualityLabel = mDevice.findObject(By.text(quality.getText()));
+            if (qualityLabel != null) {
+                break;
+            }
+            selectDialog.scroll(Direction.DOWN, 1.0f);
+            mDevice.waitForIdle();
+        }
+        if (qualityLabel == null) {
+            throw new UnknownUiException(
+                    String.format("Could not find quality %s label", quality.getText()));
+        }
+
+        Log.v(TAG, String.format("Found quality %s label", quality.getText()));
+        qualityLabel.click();
+        if (!mDevice.wait(Until.hasObject(By.res(UI_PACKAGE_NAME, UI_VIDEO_PLAYER_ID)),
+                UI_NAVIGATION_WAIT)) {
+            throw new UnknownUiException("Did not find video player after selecting quality.");
+        }
+    }
+
+    private UiObject2 getFullscreenButton() {
+        return mDevice.findObject(By.desc(UI_FULLSCREEN_BUTTON_DESC));
+    }
+
+    private UiObject2 getPlayPauseReplayButton() {
+        return mDevice.findObject(
+            By.res(UI_PACKAGE_NAME, UI_VIDEO_PLAYER_PLAY_PAUSE_REPLAY_BUTTON_ID));
+    }
+
+    public void resumeVideo() {
+        UiObject2 videoPlayer = getVideoPlayer();
+        if (videoPlayer == null) {
+            throw new UnknownUiException("Could not find video player.");
+        }
+
+        videoPlayer.click();
+        UiObject2 playPauseReplayButton = mDevice.wait(Until.findObject(By.res(UI_PACKAGE_NAME,
+                UI_VIDEO_PLAYER_PLAY_PAUSE_REPLAY_BUTTON_ID)), UI_NAVIGATION_WAIT);
+        if (playPauseReplayButton == null) {
+            throw new UnknownUiException("Could not find the pause/play button.");
+        }
+
+        if (UI_PLAY_VIDEO_DESC.equals(playPauseReplayButton.getContentDescription())) {
+            playPauseReplayButton.click();
+        }
+    }
+
+    private boolean hasConnectionEstablishedMessage() {
+        Pattern establishedMsg =
+                Pattern.compile("Connection established", Pattern.CASE_INSENSITIVE);
+        return mDevice.hasObject(By.res(UI_PACKAGE_NAME, "message").text(establishedMsg));
+    }
+
+    private void pressGoOnline() {
+        Pattern goOnlineMsg = Pattern.compile("Go online", Pattern.CASE_INSENSITIVE);
+        UiObject2 button = mDevice.findObject(By.res(UI_PACKAGE_NAME, "action").text(goOnlineMsg));
+        if (button != null) {
+            button.click();
+            mDevice.waitForIdle();
+        } else {
+            throw new UnknownUiException("Unable to find GO ONLINE button.");
+        }
+    }
+}
diff --git a/scripts/perf-setup/Android.mk b/scripts/perf-setup/Android.mk
new file mode 100644
index 0000000..2d9119c
--- /dev/null
+++ b/scripts/perf-setup/Android.mk
@@ -0,0 +1,46 @@
+#
+# 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.
+
+
+# Rules to generate setup script for device perf tests
+# Different devices may share the same script. To add a new script, define a
+# new variable named <device name>_script, pointing at the script in current
+# source folder.
+# At execution time, scripts will be pushed onto device and run with root
+# identity
+
+LOCAL_PATH:= $(call my-dir)
+
+# only define the target if a perf setup script is defined by the BoardConfig
+# of the device we are building.
+#
+# To add a new script:
+# 1. add a new setup script suitable for the device at:
+#    platform_testing/scripts/perf-setup/
+# 2. modify BoardConfig.mk of the corresponding device under:
+#    device/<OEM name>/<device name/
+# 3. add variable "BOARD_PERFSETUP_SCRIPT", and point it at the path to the new
+#    perf setup script; the path should be relative to the build root
+ifneq ($(strip $(BOARD_PERFSETUP_SCRIPT)),)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := perf-setup.sh
+LOCAL_MODULE_CLASS := EXECUTABLES
+LOCAL_MODULE_TAGS := optional
+LOCAL_MODULE_PATH := $(TARGET_OUT_DATA)/local/tmp
+LOCAL_PREBUILT_MODULE_FILE := $(BOARD_PERFSETUP_SCRIPT)
+include $(BUILD_PREBUILT)
+
+endif
diff --git a/scripts/perf-setup/angler-setup.sh b/scripts/perf-setup/angler-setup.sh
new file mode 100755
index 0000000..7080df7
--- /dev/null
+++ b/scripts/perf-setup/angler-setup.sh
@@ -0,0 +1,28 @@
+if [[ "`id -u`" -ne "0" ]]; then
+  echo "WARNING: running as non-root, proceeding anyways..."
+fi
+
+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
+
+echo -n 1 > /sys/devices/system/cpu/cpu4/online
+echo -n performance > /sys/devices/system/cpu/cpu4/cpufreq/scaling_governor
+
+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/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
+
diff --git a/tests/androidbvt/Android.mk b/tests/androidbvt/Android.mk
new file mode 100644
index 0000000..970a3df
--- /dev/null
+++ b/tests/androidbvt/Android.mk
@@ -0,0 +1,29 @@
+# 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
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT 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_TAGS := tests
+
+LOCAL_SDK_VERSION := system_current
+media_framework_app_base := frameworks/base/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest
+LOCAL_SRC_FILES := $(call all-subdir-java-files)
+LOCAL_JAVA_LIBRARIES := android.test.runner
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-test ub-uiautomator launcher-helper-lib
+
+LOCAL_PACKAGE_NAME := AndroidBvtTests
+LOCAL_CERTIFICATE := platform
+
+include $(BUILD_PACKAGE)
diff --git a/tests/androidbvt/AndroidManifest.xml b/tests/androidbvt/AndroidManifest.xml
new file mode 100644
index 0000000..c38bc88
--- /dev/null
+++ b/tests/androidbvt/AndroidManifest.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 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.androidbvt">
+    <uses-sdk android:minSdkVersion="19"
+              android:targetSdkVersion="24" />
+    <uses-feature android:name="android.hardware.camera"
+                  android:required="true" />
+
+    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.CAPTURE_VIDEO_OUTPUT" />
+    <uses-permission android:name="android.permission.CALL_PHONE" />
+    <uses-permission android:name="android.permission.CAMERA" />
+    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.GET_ACCOUNTS" />
+    <uses-permission android:name="android.permission.GET_DETAILED_TASKS" />
+    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
+    <uses-permission android:name="android.permission.SEND_SMS" />
+    <uses-permission android:name="android.permission.SET_WALLPAPER" />
+    <uses-permission android:name="android.permission.TETHER_PRIVILEGED" />
+    <uses-permission android:name="android.permission.WAKE_LOCK" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.WRITE_SETTINGS" />
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+        <activity android:label="MediaPlaybackTest"
+                android:name=".app.MediaPlaybackTestApp"
+                android:screenOrientation="landscape">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+
+    </application>
+    <instrumentation
+            android:name="android.support.test.runner.AndroidJUnitRunner"
+            android:targetPackage="com.android.androidbvt"
+            android:label="AndroidBVT Tests" />
+</manifest>
diff --git a/tests/androidbvt/res/drawable-xhdpi/stat_notify_email.png b/tests/androidbvt/res/drawable-xhdpi/stat_notify_email.png
new file mode 100644
index 0000000..23c4672
--- /dev/null
+++ b/tests/androidbvt/res/drawable-xhdpi/stat_notify_email.png
Binary files differ
diff --git a/tests/androidbvt/res/layout/surface_view.xml b/tests/androidbvt/res/layout/surface_view.xml
new file mode 100644
index 0000000..4999e5d
--- /dev/null
+++ b/tests/androidbvt/res/layout/surface_view.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+  android:layout_width="match_parent"
+  android:layout_height="match_parent"
+  android:orientation="vertical">
+
+  <FrameLayout
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+  <SurfaceView
+     android:id="@+id/surface_view"
+     android:layout_width="match_parent"
+     android:layout_height="match_parent"
+     android:layout_centerInParent="true"
+     />
+
+  <ImageView android:id="@+id/overlay_layer"
+     android:layout_width="0dip"
+     android:layout_height="392dip"/>
+
+  <VideoView
+   android:id="@+id/video_view"
+        android:layout_width="320px"
+        android:layout_height="240px"
+  />
+
+  </FrameLayout>
+
+</LinearLayout>
+
diff --git a/tests/androidbvt/res/raw/bbb.mkv b/tests/androidbvt/res/raw/bbb.mkv
new file mode 100644
index 0000000..e286e01
--- /dev/null
+++ b/tests/androidbvt/res/raw/bbb.mkv
Binary files differ
diff --git a/tests/androidbvt/src/com/android/androidbvt/AndroidBvtHelper.java b/tests/androidbvt/src/com/android/androidbvt/AndroidBvtHelper.java
new file mode 100644
index 0000000..5428ad1
--- /dev/null
+++ b/tests/androidbvt/src/com/android/androidbvt/AndroidBvtHelper.java
@@ -0,0 +1,104 @@
+/*
+ * 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.androidbvt;
+
+import android.app.DownloadManager;
+import android.app.UiAutomation;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.wifi.WifiManager;
+import android.os.ParcelFileDescriptor;
+import android.support.test.uiautomator.UiDevice;
+import android.telecom.TelecomManager;
+import android.util.Log;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Defines constants & implements common methods to be used by Framework, SysUI, System e2e BVT
+ * tests Also ensures single instance of this object
+ */
+public class AndroidBvtHelper {
+    public static final String TEST_TAG = "AndroidBVT";
+    public static final int SHORT_TIMEOUT = 1000;
+    public static final int LONG_TIMEOUT = 5000;
+    private static AndroidBvtHelper mInstance = null;
+    private Context mContext = null;
+    private UiDevice mDevice = null;
+    private UiAutomation mUiAutomation = null;
+
+    public AndroidBvtHelper(UiDevice device, Context context, UiAutomation uiAutomation) {
+        mContext = context;
+        mDevice = device;
+        mUiAutomation = uiAutomation;
+    }
+
+    public static AndroidBvtHelper getInstance(UiDevice device, Context context,
+            UiAutomation uiAutomation) {
+        if (mInstance == null) {
+            mInstance = new AndroidBvtHelper(device, context, uiAutomation);
+        }
+        return mInstance;
+    }
+
+    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);
+    }
+
+    /**
+     * Only executes 'adb shell' commands that run in the same process as the runner Converts output
+     * of the command from ParcelFileDescriptior to user friendly list of strings
+     * https://developer.android.com/reference/android/app/UiAutomation.html#executeShellCommand(
+     * java.lang.String)
+     */
+    public List<String> executeShellCommand(String cmd) {
+        if (cmd == null || cmd.isEmpty()) {
+            return null;
+        }
+        List<String> output = new ArrayList<String>();
+        ParcelFileDescriptor pfd = mUiAutomation.executeShellCommand(cmd);
+        try (BufferedReader reader = new BufferedReader(
+                new InputStreamReader(new FileInputStream(pfd.getFileDescriptor())))) {
+            String line;
+            while ((line = reader.readLine()) != null) {
+                output.add(line);
+            }
+        } catch (IOException e) {
+            Log.e(TEST_TAG, e.getMessage());
+        }
+        return output;
+    }
+}
diff --git a/tests/androidbvt/src/com/android/androidbvt/ConnectivityWifiTests.java b/tests/androidbvt/src/com/android/androidbvt/ConnectivityWifiTests.java
new file mode 100644
index 0000000..c9bbd92
--- /dev/null
+++ b/tests/androidbvt/src/com/android/androidbvt/ConnectivityWifiTests.java
@@ -0,0 +1,208 @@
+/*
+ * 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.androidbvt;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiConfiguration.AuthAlgorithm;
+import android.net.wifi.WifiConfiguration.KeyMgmt;
+import android.net.wifi.WifiManager;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.Until;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.test.suitebuilder.annotation.Suppress;
+import android.util.Log;
+
+import junit.framework.TestCase;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+public class ConnectivityWifiTests extends TestCase {
+    private final static String DEFAULT_PING_SITE = "www.google.com";
+    private final String NETWORK_ID = "AndroidAP";
+    private final String PASSWD = "androidwifi";
+    private UiDevice mDevice;
+    private WifiManager mWifiManager = null;
+    private Context mContext = null;
+    private AndroidBvtHelper mABvtHelper = null;
+    private WifiConfiguration mOriginalConfig = null;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        mDevice.setOrientationNatural();
+        mContext = InstrumentationRegistry.getTargetContext();
+        mABvtHelper = AndroidBvtHelper.getInstance(mDevice, mContext,
+                InstrumentationRegistry.getInstrumentation().getUiAutomation());
+        mWifiManager = mABvtHelper.getWifiManager();
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        mDevice.wakeUp();
+        mDevice.unfreezeRotation();
+        mDevice.pressHome();
+        mDevice.waitForIdle();
+        super.tearDown();
+    }
+
+    /**
+     * Test verifies wifi can be disconnected, disabled followed by enable and reconnect. As part of
+     * connection check, it pings a site and ensures HTTP_OK return
+     */
+    @LargeTest
+    public void testWifiConnection() throws InterruptedException {
+        // Wifi is already connected as part of tradefed device setup, assert that
+        assertTrue("Wifi should be connected", isWifiConnected());
+        assertNotNull("Wifi manager is null", mWifiManager);
+        assertTrue("Wifi isn't enabled", mWifiManager.isWifiEnabled());
+        // Disconnect wifi and disable network, save NetId to be used for re-enabling network
+        int netId = mWifiManager.getConnectionInfo().getNetworkId();
+        disconnectWifi();
+        Log.d("MyTestTag", "before sleep");
+        Thread.sleep(mABvtHelper.LONG_TIMEOUT);
+        Log.d("MyTestTag", "after sleep");
+        assertFalse("Wifi shouldn't be connected", isWifiConnected());
+        // Network enabled successfully
+        assertTrue("Network isn't enabled", mWifiManager.enableNetwork(netId, true));
+        // Allow time to settle down
+        Thread.sleep(mABvtHelper.LONG_TIMEOUT * 2);
+        assertTrue("Wifi should be connected", isWifiConnected());
+    }
+
+    /**
+     * Test verifies from UI that bunch of AP are listed on enabling Wifi
+     */
+    @LargeTest
+    public void testWifiDiscoveredAPShownUI() throws InterruptedException {
+        Intent intent_as = new Intent(
+                android.provider.Settings.ACTION_WIFI_SETTINGS);
+        mContext.startActivity(intent_as);
+        Thread.sleep(mABvtHelper.LONG_TIMEOUT);
+        assertNotNull("AP list shouldn't be null",
+                mDevice.wait(Until.findObject(By.res("com.android.settings:id/list")),
+                        mABvtHelper.LONG_TIMEOUT));
+        assertTrue("At least 1 AP should be visible",
+                mDevice.wait(Until.findObject(By.res("com.android.settings:id/list")),
+                        mABvtHelper.LONG_TIMEOUT)
+                        .getChildren().size() > 0);
+    }
+
+    /**
+     * Verifies WifiAp is by default disabled Then enable adn disable it
+     */
+    @LargeTest
+    @Suppress
+    public void testWifiTetheringDisableEnable() throws InterruptedException {
+        WifiConfiguration config = new WifiConfiguration();
+        config.SSID = NETWORK_ID;
+        config.allowedKeyManagement.set(KeyMgmt.WPA_PSK);
+        config.allowedAuthAlgorithms.set(AuthAlgorithm.OPEN);
+        config.preSharedKey = PASSWD;
+        int counter;
+        try {
+            // disable wifiap
+            assertTrue("wifi hotspot not disabled by default",
+                    mWifiManager.getWifiApState() == WifiManager.WIFI_AP_STATE_DISABLED);
+            // Enable wifiap
+            assertTrue("failed to disable wifi hotspot",
+                    mWifiManager.setWifiApEnabled(config, true));
+            Log.d("MyTestTag", "Now checkign wifi ap");
+            counter = 10;
+            while (--counter > 0
+                    && mWifiManager.getWifiApState() != WifiManager.WIFI_AP_STATE_ENABLED) {
+                Thread.sleep(mABvtHelper.SHORT_TIMEOUT);
+            }
+            assertTrue("wifi hotspot not enabled",
+                    mWifiManager.getWifiApState() == WifiManager.WIFI_AP_STATE_ENABLED);
+            // Navigate to Wireless Settings page and verify Wifi AP setting is on
+            Intent intent_as = new Intent(
+                    android.provider.Settings.ACTION_WIRELESS_SETTINGS);
+            mContext.startActivity(intent_as);
+            Thread.sleep(mABvtHelper.LONG_TIMEOUT);
+            mDevice.wait(Until.findObject(By.text("Tethering & portable hotspot")),
+                    mABvtHelper.LONG_TIMEOUT).click();
+            Thread.sleep(mABvtHelper.SHORT_TIMEOUT);
+            assertTrue("Settings UI for Wifi AP is not ON",
+                    mDevice.wait(Until.hasObject(By.text("Portable hotspot AndroidAP active")),
+                            mABvtHelper.LONG_TIMEOUT));
+
+            mDevice.wait(Until.findObject(By.text("Portable Wi‑Fi hotspot")),
+                    mABvtHelper.LONG_TIMEOUT).click();
+            assertTrue("Wifi ap disable call fails", mWifiManager.setWifiApEnabled(config,
+                    false));
+            counter = 5;
+            while (--counter > 0
+                    && mWifiManager.getWifiApState() != WifiManager.WIFI_AP_STATE_DISABLED) {
+                Thread.sleep(mABvtHelper.LONG_TIMEOUT);
+            }
+            assertTrue("wifi hotspot not enabled",
+                    mWifiManager.getWifiApState() == WifiManager.WIFI_AP_STATE_DISABLED);
+            Thread.sleep(mABvtHelper.LONG_TIMEOUT * 2);
+        } finally {
+            assertTrue("Wifi enable call fails", mWifiManager
+                    .enableNetwork(mWifiManager.getConnectionInfo().getNetworkId(), false));
+            counter = 10;
+            while (--counter > 0 && !mWifiManager.isWifiEnabled()) {
+                Thread.sleep(mABvtHelper.LONG_TIMEOUT);
+            }
+            assertTrue("Wifi isn't enabled", mWifiManager.isWifiEnabled());
+        }
+    }
+
+    /**
+     * Checks if wifi connection is active by sending an HTTP request, check for HTTP_OK
+     */
+    private boolean isWifiConnected() throws InterruptedException {
+        int counter = 10;
+        while (--counter > 0) {
+            try {
+                String mPingSite = String.format("http://%s", DEFAULT_PING_SITE);
+                URL url = new URL(mPingSite);
+                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+                conn.setRequestMethod("GET");
+                conn.setConnectTimeout(mABvtHelper.LONG_TIMEOUT * 5);
+                conn.setReadTimeout(mABvtHelper.LONG_TIMEOUT * 5);
+                if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
+                    return true;
+                }
+                Thread.sleep(mABvtHelper.SHORT_TIMEOUT);
+            } catch (IOException ex) {
+                // Wifi being flaky in the lab, test retries 5 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(mABvtHelper.TEST_TAG, ex.getMessage());
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Disconnects and disables network
+     */
+    private void disconnectWifi() {
+        assertTrue("Wifi not disconnected", mWifiManager.disconnect());
+        mWifiManager.disableNetwork(mWifiManager.getConnectionInfo().getNetworkId());
+        mWifiManager.saveConfiguration();
+    }
+}
diff --git a/tests/androidbvt/src/com/android/androidbvt/FrameworkDownloadTests.java b/tests/androidbvt/src/com/android/androidbvt/FrameworkDownloadTests.java
new file mode 100644
index 0000000..9f14d02
--- /dev/null
+++ b/tests/androidbvt/src/com/android/androidbvt/FrameworkDownloadTests.java
@@ -0,0 +1,175 @@
+/*
+ * 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.androidbvt;
+
+import android.app.DownloadManager;
+import android.app.DownloadManager.Query;
+import android.app.DownloadManager.Request;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.database.Cursor;
+import android.net.Uri;
+import android.net.wifi.WifiManager;
+import android.os.ParcelFileDescriptor;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.uiautomator.UiDevice;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+
+import junit.framework.TestCase;
+
+import java.io.IOException;
+import java.util.HashSet;
+
+public class FrameworkDownloadTests extends TestCase {
+    private static final String TEST_TAG = "AndroidBVT";
+    private final String TEST_HOST = "209.119.80.137:10090/new_ui/all_content/photos";
+    private final String TEST_FILE = "android_apps.jpeg";
+    private final int TEST_FILE_SIZE = 159709;
+    private DownloadManager mDownloadManager = null;
+    private WifiManager mWifiManager = null;
+    private AndroidBvtHelper mABvtHelper = null;
+    private UiDevice mDevice;
+    private Context mContext = null;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        mDevice.freezeRotation();
+        mContext = InstrumentationRegistry.getTargetContext();
+        mABvtHelper = AndroidBvtHelper.getInstance(mDevice, mContext,
+                InstrumentationRegistry.getInstrumentation().getUiAutomation());
+        mDownloadManager = mABvtHelper.getDownloadManager();
+        mWifiManager = mABvtHelper.getWifiManager();
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        mDevice.unfreezeRotation();
+        mDevice.pressMenu();
+        mDevice.waitForIdle();
+        super.tearDown();
+    }
+
+    /**
+     * Following test verifies that download service is running and serves any download request
+     * Enqueues a request to download a photo After download completion, compares file size that
+     * mentioned in server
+     */
+    @LargeTest
+    public void testPhotoDownloadSucceed() throws InterruptedException, IOException {
+        // Device already connected to wifi as part of tradefed setup
+        if (!mWifiManager.isWifiEnabled()) {
+            mWifiManager.enableNetwork(mWifiManager.getConnectionInfo().getNetworkId(), true);
+            int counter = 5;
+            while (--counter > 0 && mWifiManager.isWifiEnabled()) {
+                Thread.sleep(mABvtHelper.LONG_TIMEOUT);
+            }
+        }
+        assertTrue("Wifi should be enabled by now", mWifiManager.isWifiEnabled());
+        removeAllCurrentDownloads(); // if there are any in progress
+        Uri downloadUri = Uri.parse(String.format("http://%s/%s", TEST_HOST, TEST_FILE));
+        Request request = new Request(downloadUri);
+        long dlRequest = mDownloadManager.enqueue(request);
+
+        // Register receiver to listen to DownloadComplete Broadcase message
+        // Wait for download to finish
+        final DownloadCompleteReceiver receiver = new DownloadCompleteReceiver();
+        try {
+            IntentFilter intentFilter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
+            mContext.registerReceiver(receiver, intentFilter);
+            Thread.sleep(mABvtHelper.LONG_TIMEOUT);
+            assertTrue("download not finished", receiver.isDownloadCompleted(dlRequest));
+            // Verify Download file size
+            ParcelFileDescriptor pfd = null;
+            try {
+                pfd = mDownloadManager.openDownloadedFile(dlRequest);
+                assertTrue("File size should be same as mentioned in server",
+                        pfd.getStatSize() == TEST_FILE_SIZE);
+            } finally {
+                if (pfd != null) {
+                    pfd.close();
+                }
+                mDownloadManager.remove(dlRequest);
+            }
+        } finally {
+            mContext.unregisterReceiver(receiver);
+        }
+    }
+
+    /**
+     * Remove all downloads those are in progress now
+     */
+    private void removeAllCurrentDownloads() {
+        DownloadManager downloadManager = (DownloadManager) mContext
+                .getSystemService(Context.DOWNLOAD_SERVICE);
+        Cursor cursor = downloadManager.query(new Query());
+        try {
+            if (cursor.moveToFirst()) {
+                do {
+                    int index = cursor.getColumnIndex(DownloadManager.COLUMN_ID);
+                    long downloadId = cursor.getLong(index);
+                    downloadManager.remove(downloadId);
+                } while (cursor.moveToNext());
+            }
+        } finally {
+            cursor.close();
+        }
+    }
+
+    /**
+     * DownloadManager broadcasts 'DownloadManager.ACTION_DOWNLOAD_COMPLETE' once download is
+     * complete and copied from cache to Downloads folder Following receiver to intercept download
+     * intent to parse out the downloaded id to ensure that the item has been downloaded that was
+     * initiated in the test. Please note that when a download action is enqueued, DownloadManager
+     * provides a download id
+     */
+    private class DownloadCompleteReceiver extends BroadcastReceiver {
+        private HashSet<Long> mCompleteIds = new HashSet<>();
+
+        public DownloadCompleteReceiver() {
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            synchronized (mCompleteIds) {
+                mCompleteIds.add(intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1));
+                Log.i(TEST_TAG, "Request Id = "
+                        + intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1));
+                mCompleteIds.notifyAll();
+            }
+        }
+
+        // Tries 5 times/5 secs for download to be completed
+        public boolean isDownloadCompleted(long id)
+                throws InterruptedException {
+            int counter = 10;
+            while (--counter > 0) {
+                synchronized (mCompleteIds) {
+                    mCompleteIds.wait(mABvtHelper.LONG_TIMEOUT);
+                    if (mCompleteIds.contains(id)) {
+                        return true;
+                    }
+                }
+            }
+            return false;
+        }
+    }
+}
diff --git a/tests/androidbvt/src/com/android/androidbvt/MediaCaptureTests.java b/tests/androidbvt/src/com/android/androidbvt/MediaCaptureTests.java
new file mode 100644
index 0000000..518a49d
--- /dev/null
+++ b/tests/androidbvt/src/com/android/androidbvt/MediaCaptureTests.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 com.android.androidbvt;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.MediaStore;
+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.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+
+import junit.framework.TestCase;
+import java.io.File;
+import java.util.regex.Pattern;
+
+/**
+ * Basic tests for the Camera app.
+ */
+public class MediaCaptureTests extends TestCase {
+    private static final int CAPTURE_TIMEOUT = 6000;
+    private static final String DESC_BTN_CAPTURE_PHOTO = "Capture photo";
+    private static final String DESC_BTN_CAPTURE_VIDEO = "Capture video";
+    private static final String DESC_BTN_DONE = "Done";
+    private static final String DESC_BTN_PHOTO_MODE = "Open photo mode";
+    private static final String DESC_BTN_VIDEO_MODE = "Open video mode";
+    private static final int FILE_CHECK_ATTEMPTS = 5;
+    private static final int VIDEO_LENGTH = 2000;
+
+    private UiDevice mDevice;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        mDevice.freezeRotation();
+        // if there are any dialogues that pop up, dismiss them
+        UiObject2 maybeOkButton = mDevice.wait(Until.findObject(By.res("android:id/ok_button")),
+                CAPTURE_TIMEOUT);
+        if (maybeOkButton != null) {
+            maybeOkButton.click();
+        }
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        mDevice.unfreezeRotation();
+        super.tearDown();
+    }
+
+    /**
+     * Test that the device can capture a photo.
+     */
+    @LargeTest
+    public void testPhotoCapture() {
+        runCaptureTest(new Intent(MediaStore.ACTION_IMAGE_CAPTURE), "smoke.jpg", false);
+    }
+
+    /**
+     * Test that the device can capture a video.
+     */
+    @LargeTest
+    public void testVideoCapture() {
+        runCaptureTest(new Intent(MediaStore.ACTION_VIDEO_CAPTURE), "smoke.avi", true);
+    }
+
+    private void runCaptureTest(Intent intent, String tmpName, boolean isVideo) {
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        if (intent.resolveActivity(
+                InstrumentationRegistry.getInstrumentation().getContext().getPackageManager()) != null) {
+            File outputFile = null;
+            try {
+                outputFile = new File(Environment
+                        .getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM), tmpName);
+                intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(outputFile));
+                InstrumentationRegistry.getInstrumentation().getContext().startActivity(intent);
+                switchCaptureMode(isVideo);
+                pressCaptureButton(isVideo);
+                if (isVideo) {
+                    Thread.sleep(VIDEO_LENGTH);
+                    pressCaptureButton(isVideo);
+                }
+                Thread.sleep(1000);
+                pushButton(DESC_BTN_DONE);
+                long fileLength = outputFile.length();
+                for (int i=0; i<FILE_CHECK_ATTEMPTS; i++) {
+                    if ((fileLength = outputFile.length()) > 0) {
+                        break;
+                    }
+                    Thread.sleep(1000);
+                }
+                assertTrue(fileLength > 0);
+            } catch (InterruptedException e) {
+                fail(e.getLocalizedMessage());
+            } finally {
+                if (outputFile != null) {
+                    outputFile.delete();
+                }
+            }
+        }
+    }
+
+    private void switchCaptureMode(boolean isVideo) {
+        if (isVideo) {
+            pushButton(DESC_BTN_VIDEO_MODE);
+        } else {
+            pushButton(DESC_BTN_PHOTO_MODE);
+        }
+    }
+
+    private void pressCaptureButton(boolean isVideo) {
+        if (isVideo) {
+            pushButton(DESC_BTN_CAPTURE_VIDEO);
+        } else {
+            pushButton(DESC_BTN_CAPTURE_PHOTO);
+        }
+    }
+
+    private void pushButton(String desc) {
+        Pattern pattern = Pattern.compile(desc, Pattern.CASE_INSENSITIVE);
+        UiObject2 doneBtn = mDevice.wait(Until.findObject(By.desc(pattern)), CAPTURE_TIMEOUT);
+        if (null != doneBtn) {
+            doneBtn.clickAndWait(Until.newWindow(), 500);
+        }
+    }
+}
diff --git a/tests/androidbvt/src/com/android/androidbvt/MediaPlaybackTests.java b/tests/androidbvt/src/com/android/androidbvt/MediaPlaybackTests.java
new file mode 100644
index 0000000..474e820
--- /dev/null
+++ b/tests/androidbvt/src/com/android/androidbvt/MediaPlaybackTests.java
@@ -0,0 +1,109 @@
+/*
+ * 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.androidbvt;
+
+import android.media.MediaPlayer;
+import android.os.Looper;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+
+import com.android.androidbvt.app.MediaPlaybackTestApp;
+
+/**
+ * Basic tests for video playback
+ */
+public class MediaPlaybackTests extends ActivityInstrumentationTestCase2<MediaPlaybackTestApp> {
+
+    private static final String TAG = "MediaPlaybackTest";
+    private static final int LOOP_START_BUFFER_MS = 10000;
+    private static final int PLAY_BUFFER_MS = 2000;
+    private final Object mCompletionLock = new Object();
+    private final Object mLooperLock = new Object();
+    private boolean mPlaybackSucceeded = false;
+    private boolean mPlaybackError = false;
+    private Looper mLooper;
+    private MediaPlayer mPlayer;
+
+    public MediaPlaybackTests() {
+        super(MediaPlaybackTestApp.class);
+    }
+
+    @Override
+    public void setUp() throws Exception {
+        // start activity
+        getActivity();
+    }
+
+    @LargeTest
+    public void testVideoPlayback() {
+        // start the MediaPlayer on a Looper thread, so it does not deadlock itself
+        new Thread() {
+            @Override
+            public void run() {
+                Looper.prepare();
+                mLooper = Looper.myLooper();
+                mPlayer = MediaPlayer.create(getInstrumentation().getContext(), R.raw.bbb);
+                mPlayer.setDisplay(getActivity().getSurfaceHolder());
+                synchronized (mLooperLock) {
+                    mLooperLock.notify();
+                }
+                Looper.loop();
+            }
+        }.start();
+        // make sure the looper is really started before we proceed
+        synchronized (mLooperLock) {
+            try {
+                mLooperLock.wait(LOOP_START_BUFFER_MS);
+            } catch (InterruptedException e) {
+                fail("Loop thread start was interrupted");
+            }
+        }
+        mPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
+            @Override
+            public boolean onError(MediaPlayer mp, int what, int extra) {
+                mPlaybackError = true;
+                mp.reset();
+                return true;
+            }
+        });
+        mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
+            @Override
+            public void onCompletion(MediaPlayer mp) {
+                synchronized (mCompletionLock) {
+                    Log.w(TAG, "Hit onCompletion!");
+                    mPlaybackSucceeded = true;
+                    mCompletionLock.notifyAll();
+                }
+            }
+        });
+        mPlayer.start();
+        int duration = mPlayer.getDuration();
+        int currentPosition = mPlayer.getCurrentPosition();
+        synchronized (mCompletionLock) {
+            try {
+                mCompletionLock.wait(duration - currentPosition + PLAY_BUFFER_MS);
+            } catch (InterruptedException e) {
+                fail("Wait for playback was interrupted");
+            }
+        }
+        mLooper.quit();
+        mPlayer.release();
+        assertFalse(mPlaybackError);
+        assertTrue(mPlaybackSucceeded);
+    }
+}
diff --git a/tests/androidbvt/src/com/android/androidbvt/SysUIGSATests.java b/tests/androidbvt/src/com/android/androidbvt/SysUIGSATests.java
new file mode 100644
index 0000000..dd7a9cf
--- /dev/null
+++ b/tests/androidbvt/src/com/android/androidbvt/SysUIGSATests.java
@@ -0,0 +1,169 @@
+/*
+ * 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.androidbvt;
+
+import android.app.UiAutomation;
+import android.content.Context;
+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.support.test.uiautomator.Until;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.view.KeyEvent;
+
+import junit.framework.TestCase;
+
+import java.util.List;
+
+/**
+ * Contains tests for features that are loosely coupled with Android system for sanity
+ */
+public class SysUIGSATests extends TestCase {
+    private final String QSB_PKG = "com.google.android.googlequicksearchbox";
+    private UiAutomation mUiAutomation = null;
+    private UiDevice mDevice;
+    private Context mContext = null;
+    private AndroidBvtHelper mABvtHelper = null;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        mDevice.setOrientationNatural();
+        mContext = InstrumentationRegistry.getTargetContext();
+        mUiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        mABvtHelper = AndroidBvtHelper.getInstance(mDevice, mContext, mUiAutomation);
+        mDevice.pressMenu();
+        mDevice.pressHome();
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        mDevice.pressHome();
+        mDevice.unfreezeRotation();
+        super.tearDown();
+    }
+
+    /**
+     * Ensures search via QSB searches both web and device apps Suuggested texts starts with
+     * searched text Remembers searched item, suggests as top suggestion next time
+     */
+    @LargeTest
+    public void testGoogleQuickSearchBar() throws InterruptedException {
+        final String TextToSearch = "co";
+        UiObject2 searchBox = null;
+        int counter = 5;
+        while (--counter > 0
+                && ((searchBox = mDevice.wait(Until.findObject(By.res(QSB_PKG, "search_box")),
+                        mABvtHelper.SHORT_TIMEOUT)) == null)) {
+            Thread.sleep(mABvtHelper.SHORT_TIMEOUT);
+            mDevice.pressHome();
+            mDevice.pressSearch();
+        }
+        mDevice.wait(Until.findObject(By.res(QSB_PKG, "search_box")),
+                mABvtHelper.LONG_TIMEOUT).setText(TextToSearch);
+        Thread.sleep(mABvtHelper.LONG_TIMEOUT);
+        // make the IME down
+        mDevice.pressKeyCode(KeyEvent.KEYCODE_BACK);
+        // searching for 'co' will result from web, as well as 'Contacts' app. So there should be
+        // more than 1 container
+        UiObject2 searchSuggestionsContainer = mDevice.wait(Until.findObject(By.res(
+                QSB_PKG, "search_suggestions_container")), mABvtHelper.LONG_TIMEOUT);
+        assertTrue("QS suggestion should have more than 1 container",
+                searchSuggestionsContainer.getChildCount() > 1);
+        UiObject2 searchSuggestions = mDevice.wait(Until.findObject(By.res(
+                QSB_PKG, "search_suggestions_web")), mABvtHelper.LONG_TIMEOUT);
+        assertNotNull(
+                "Web Search suggestions shouldn't be null & should have more than 1 suggestions",
+                searchSuggestions != null && searchSuggestions.getChildCount() > 1);
+        List<UiObject2> suggestions = mDevice.wait(Until.findObjects(By.res(QSB_PKG, "text_1")),
+                mABvtHelper.LONG_TIMEOUT);
+        assertNotNull("Contacts app should be found", mDevice.wait(Until.findObject(
+                By.res(QSB_PKG, "text_1").text("Contacts")), mABvtHelper.LONG_TIMEOUT));
+        String topSuggestedText = suggestions.get(0).getText();
+        suggestions.get(0).clickAndWait(Until.newWindow(), mABvtHelper.LONG_TIMEOUT);
+        Thread.sleep(mABvtHelper.LONG_TIMEOUT);
+        // Search again and ensure last searched item showed as top suggestion
+        mDevice.pressHome();
+        Thread.sleep(mABvtHelper.SHORT_TIMEOUT);
+        mDevice.pressSearch();
+        String currentTopSuggestion = mDevice.wait(Until.findObjects(By.res(QSB_PKG, "text_1")),
+                mABvtHelper.LONG_TIMEOUT).get(0).getText();
+        assertTrue("Previous searched item isn't top suggested word",
+                topSuggestedText.toLowerCase().equals(topSuggestedText.toLowerCase()));
+    }
+
+    /**
+     * Ensures if any account is opted in GoogleNow, Google-assist offers card on long home press
+     */
+    @LargeTest
+    public void testGoogleAssist() throws InterruptedException {
+        mDevice.wait(Until.findObject(By.res(QSB_PKG, "search_plate")),
+                mABvtHelper.SHORT_TIMEOUT).click();
+        Thread.sleep(mABvtHelper.SHORT_TIMEOUT);
+        UiObject2 getStarted = mDevice.wait(Until.findObject(By.text("GET STARTED")),
+                mABvtHelper.SHORT_TIMEOUT);
+        if (getStarted != null) {
+            getStarted.clickAndWait(Until.newWindow(), mABvtHelper.SHORT_TIMEOUT);
+            mDevice.wait(Until.findObject(By.res(QSB_PKG, "text_container")),
+                    mABvtHelper.SHORT_TIMEOUT).swipe(Direction.UP, 1.0f);
+            mDevice.wait(Until.findObject(By.text("YES, I’M IN")),
+                    mABvtHelper.SHORT_TIMEOUT)
+                    .clickAndWait(Until.newWindow(), mABvtHelper.SHORT_TIMEOUT);
+        }
+        // Search for Paris and click on first suggested text
+        mDevice.wait(Until.findObject(By.res(QSB_PKG, "text_container")),
+                mABvtHelper.LONG_TIMEOUT).setText("Paris");
+        Thread.sleep(mABvtHelper.LONG_TIMEOUT);
+        List<UiObject2> suggestedTexts = null;
+        int counter = 5;
+        while (--counter > 0
+                && ((suggestedTexts = mDevice.wait(Until.findObjects(By.res(QSB_PKG, "text_1")),
+                        mABvtHelper.LONG_TIMEOUT)) == null)) {
+            Thread.sleep(mABvtHelper.SHORT_TIMEOUT);
+        }
+        assertNotNull("Suggested text shouldn't be null", suggestedTexts);
+        UiObject2 itemToClick = suggestedTexts.get(0);
+        for (UiObject2 item : suggestedTexts) {
+            if (item.getText().toLowerCase().equals("paris")) {
+                itemToClick = item;
+            }
+        }
+        itemToClick.clickAndWait(Until.newWindow(), mABvtHelper.SHORT_TIMEOUT);
+        Thread.sleep(mABvtHelper.LONG_TIMEOUT);
+        // Now long press home to load assist layer
+        mDevice.pressKeyCode(KeyEvent.KEYCODE_ASSIST);
+        UiObject2 optInYes = mDevice.wait(
+                Until.findObject(By.res(QSB_PKG, "screen_assist_opt_in_yes")),
+                mABvtHelper.SHORT_TIMEOUT);
+        if (optInYes != null) {
+            optInYes.clickAndWait(Until.newWindow(), mABvtHelper.SHORT_TIMEOUT);
+        }
+        // Ensure some cards are loaded
+        // Note card's content isn't verified
+        counter = 5;
+        UiObject2 cardContainer = null;
+        while (--counter > 0 && ((cardContainer = mDevice.wait(
+                Until.findObject(By.res(QSB_PKG, "card_container")),
+                mABvtHelper.SHORT_TIMEOUT)) != null)) {
+            Thread.sleep(mABvtHelper.SHORT_TIMEOUT);
+        }
+        assertNotNull("Some cards should be loaded", cardContainer);
+    }
+}
diff --git a/tests/androidbvt/src/com/android/androidbvt/SysUILauncherTests.java b/tests/androidbvt/src/com/android/androidbvt/SysUILauncherTests.java
new file mode 100644
index 0000000..e209817
--- /dev/null
+++ b/tests/androidbvt/src/com/android/androidbvt/SysUILauncherTests.java
@@ -0,0 +1,157 @@
+/*
+ * 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.androidbvt;
+
+import android.app.WallpaperManager;
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.drawable.Drawable;
+import android.os.RemoteException;
+import android.provider.Settings;
+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.Until;
+import android.test.suitebuilder.annotation.LargeTest;
+
+import junit.framework.TestCase;
+
+import java.io.IOException;
+import java.util.List;
+
+public class SysUILauncherTests extends TestCase {
+    private static final int LONG_TIMEOUT = 5000;
+    private static final String APP_NAME = "Clock";
+    private static final String PKG_NAME = "com.google.android.deskclock";
+    private static final String WIDGET_PREVIEW = "widget_preview";
+    private static final String APP_WIDGET_VIEW = "android.appwidget.AppWidgetHostView";
+    private static final String WIDGET_TEXT_VIEW = "android.widget.TextView";
+    private UiDevice mDevice = null;
+    private Context mContext;
+    private ILauncherStrategy mLauncherStrategy = null;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        mContext = InstrumentationRegistry.getTargetContext();
+        mDevice.setOrientationNatural();
+        mLauncherStrategy = LauncherStrategyFactory.getInstance(mDevice).getLauncherStrategy();
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        mDevice.pressHome();
+        mDevice.unfreezeRotation();
+        mDevice.waitForIdle();
+        super.tearDown();
+    }
+
+    /**
+     * Add and remove a widget from home screen
+     */
+    @LargeTest
+    public void testAddAndRemoveWidget() throws InterruptedException, IOException {
+        // press menu key
+        mDevice.pressMenu();
+        Thread.sleep(LONG_TIMEOUT);
+        mDevice.wait(Until.findObject(By.clazz(WIDGET_TEXT_VIEW)
+                .text("WIDGETS")), LONG_TIMEOUT).click();
+        Thread.sleep(LONG_TIMEOUT);
+        // Long click to add widget
+        mDevice.wait(
+                Until.findObject(
+                        By.res(mDevice.getLauncherPackageName(), WIDGET_PREVIEW)),
+                LONG_TIMEOUT).click(1000);
+        mDevice.pressHome();
+        UiObject2 appWidget = mDevice.wait(
+                Until.findObject(By.clazz(APP_WIDGET_VIEW)), LONG_TIMEOUT);
+        assertNotNull("Widget has not been added", appWidget);
+        removeObject(appWidget);
+        appWidget = mDevice.wait(Until.findObject(By.clazz(APP_WIDGET_VIEW)),
+                LONG_TIMEOUT);
+        assertNull("Widget is still there", appWidget);
+    }
+
+    /**
+     * Change Wall Paper
+     */
+    @LargeTest
+    public void testChangeWallPaper() throws InterruptedException, IOException {
+        try {
+            WallpaperManager wallpaperManagerPre = WallpaperManager.getInstance(mContext);
+            wallpaperManagerPre.clear();
+            Thread.sleep(LONG_TIMEOUT);
+            Drawable wallPaperPre = wallpaperManagerPre.getDrawable().getCurrent();
+            // press menu key
+            mDevice.pressMenu();
+            Thread.sleep(LONG_TIMEOUT);
+            mDevice.wait(Until.findObject(By.clazz(WIDGET_TEXT_VIEW)
+                    .text("WALLPAPERS")), LONG_TIMEOUT).click();
+            Thread.sleep(LONG_TIMEOUT);
+            // set second wall paper as current wallpaper for home screen and lockscreen
+            mDevice.wait(Until.findObject(By.descContains("Wallpaper 2")), LONG_TIMEOUT).click();
+            mDevice.wait(Until.findObject(By.text("Set wallpaper")), LONG_TIMEOUT).click();
+            UiObject2 homeScreen = mDevice
+                    .wait(Until.findObject(By.text("Home screen and lock screen")), LONG_TIMEOUT);
+            if (homeScreen != null) {
+                homeScreen.click();
+            }
+            Thread.sleep(LONG_TIMEOUT);
+            WallpaperManager wallpaperManagerPost = WallpaperManager.getInstance(mContext);
+            Drawable wallPaperPost = wallpaperManagerPost.getDrawable().getCurrent();
+            assertFalse("Wallpaper has not been changed", wallPaperPre.equals(wallPaperPost));
+        } finally {
+            WallpaperManager wallpaperManagerCurrrent = WallpaperManager.getInstance(mContext);
+            wallpaperManagerCurrrent.clear();
+            Thread.sleep(LONG_TIMEOUT);
+        }
+    }
+
+    /**
+     * Add and remove short cut from home screen
+     */
+    @LargeTest
+    public void testAddAndRemoveShortCut() throws InterruptedException {
+        mLauncherStrategy.openAllApps(true);
+        Thread.sleep(LONG_TIMEOUT);
+        // This is a long press and should add the shortcut to the Home screen
+        mDevice.wait(Until.findObject(By.clazz("android.widget.TextView")
+                .desc(APP_NAME)), LONG_TIMEOUT).click(1000);
+        // Searching for the object on the Home screen
+        UiObject2 app = mDevice.wait(Until.findObject(By.text(APP_NAME)), LONG_TIMEOUT);
+        assertNotNull("Apps has been added", app);
+        removeObject(app);
+        app = mDevice.wait(Until.findObject(By.text(APP_NAME)), LONG_TIMEOUT);
+        assertNull(APP_NAME + " is still there", app);
+    }
+
+    /**
+     * Remove object from home screen
+     */
+    private void removeObject(UiObject2 app) throws InterruptedException {
+        // Drag shortcut/widget icon to Remove button which behinds Google Search bar
+        UiObject2 removeButton = mDevice.wait(Until.findObject(By.desc("Google Search")),
+                LONG_TIMEOUT);
+        app.drag(new Point(removeButton.getVisibleCenter().x, removeButton.getVisibleCenter().y),
+                1000);
+    }
+}
diff --git a/tests/androidbvt/src/com/android/androidbvt/SysUILockScreenTests.java b/tests/androidbvt/src/com/android/androidbvt/SysUILockScreenTests.java
new file mode 100644
index 0000000..28192d6
--- /dev/null
+++ b/tests/androidbvt/src/com/android/androidbvt/SysUILockScreenTests.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 com.android.androidbvt;
+
+import android.app.KeyguardManager;
+import android.content.Context;
+import android.content.Intent;
+import android.os.RemoteException;
+import android.provider.Settings;
+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.support.test.uiautomator.Until;
+import android.test.suitebuilder.annotation.LargeTest;
+
+import junit.framework.Assert;
+import junit.framework.TestCase;
+
+public class SysUILockScreenTests extends TestCase {
+    private static final String LAUNCHER_PACKAGE = "com.google.android.googlequicksearchbox";
+    private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
+    private static final String EDIT_TEXT_CLASS_NAME = "android.widget.EditText";
+    private static final int SHORT_TIMEOUT = 200;
+    private static final int LONG_TIMEOUT = 2000;
+    private static final int PIN = 1234;
+    private static final String PASSWORD = "aaaa";
+    private AndroidBvtHelper mABvtHelper = null;
+    private UiDevice mDevice = null;
+    private Context mContext;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        mDevice.freezeRotation();
+        mContext = InstrumentationRegistry.getTargetContext();
+        mABvtHelper = AndroidBvtHelper.getInstance(mDevice, mContext,
+                InstrumentationRegistry.getInstrumentation().getUiAutomation());
+        mDevice.wakeUp();
+        mDevice.pressHome();
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        mDevice.pressHome();
+        mDevice.unfreezeRotation();
+        mDevice.waitForIdle();
+        super.tearDown();
+    }
+
+    /**
+     * Following test will add PIN for Lock Screen, and remove PIN
+     * @throws Exception
+     */
+    @LargeTest
+    public void testLockScreenPIN() throws Exception {
+        setScreenLock(Integer.toString(PIN), "PIN");
+        sleepAndWakeUpDevice();
+        unlockScreen(Integer.toString(PIN));
+        removeScreenLock(Integer.toString(PIN));
+        Thread.sleep(mABvtHelper.LONG_TIMEOUT);
+        Assert.assertFalse("Lock Screen is still enabled", isLockScreenEnabled());
+    }
+
+    /**
+     * Following test will add password for Lock Screen, and remove Password
+     * @throws Exception
+     */
+    @LargeTest
+    public void testLockScreenPwd() throws Exception {
+        setScreenLock(PASSWORD, "Password");
+        sleepAndWakeUpDevice();
+        unlockScreen(PASSWORD);
+        removeScreenLock(PASSWORD);
+        Thread.sleep(mABvtHelper.LONG_TIMEOUT);
+        Assert.assertFalse("Lock Screen is still enabled", isLockScreenEnabled());
+    }
+
+    /**
+     * Following test will add password for Lock Screen, check Emergency Call Page existence, and
+     * remove password for Lock Screen
+     * @throws Exception
+     */
+    @LargeTest
+    public void testEmergencyCall() throws Exception {
+        setScreenLock(PASSWORD, "Password");
+        sleepAndWakeUpDevice();
+        checkCheckEmergencyCall();
+        unlockScreen(PASSWORD);
+        removeScreenLock(PASSWORD);
+        Thread.sleep(mABvtHelper.LONG_TIMEOUT);
+        Assert.assertFalse("Lock Screen is still enabled", isLockScreenEnabled());
+    }
+
+    /**
+     * Just lock the screen and slide up to unlock
+     */
+    @LargeTest
+    public void testSlideUnlock() throws Exception {
+        sleepAndWakeUpDevice();
+        mDevice.wait(Until.findObject(
+                By.res(SYSTEMUI_PACKAGE, "notification_stack_scroller")), 2000)
+                .swipe(Direction.UP, 1.0f);
+        int counter = 6;
+        UiObject2 workspace = mDevice.findObject(By.res(LAUNCHER_PACKAGE, "workspace"));
+        while (counter-- > 0 && workspace == null) {
+            workspace = mDevice.findObject(By.res(LAUNCHER_PACKAGE, "workspace"));
+            Thread.sleep(500);
+        }
+        assertNotNull("Workspace wasn't found", workspace);
+    }
+
+    /**
+     * Sets the screen lock pin or password
+     * @param pwd text of Password or Pin for lockscreen
+     * @param mode indicate if its password or PIN
+     */
+    private void setScreenLock(String pwd, String mode) throws Exception {
+        navigateToScreenLock();
+        mDevice.wait(Until.findObject(By.text(mode)), mABvtHelper.LONG_TIMEOUT).click();
+        // set up Secure start-up page
+        mDevice.wait(Until.findObject(By.text("No thanks")), mABvtHelper.LONG_TIMEOUT).click();
+        UiObject2 pinField = mDevice.wait(Until.findObject(By.clazz(EDIT_TEXT_CLASS_NAME)),
+                mABvtHelper.LONG_TIMEOUT);
+        pinField.setText(pwd);
+        // enter and verify password
+        mDevice.pressEnter();
+        pinField.setText(pwd);
+        mDevice.pressEnter();
+        mDevice.wait(Until.findObject(By.text("DONE")), mABvtHelper.LONG_TIMEOUT).click();
+    }
+
+    /**
+     * check if Emergency Call page exists
+     */
+    private void checkCheckEmergencyCall() throws Exception {
+        mDevice.pressMenu();
+        mDevice.wait(Until.findObject(By.text("EMERGENCY")), mABvtHelper.LONG_TIMEOUT).click();
+        Thread.sleep(mABvtHelper.LONG_TIMEOUT);
+        UiObject2 dialButton = mDevice.wait(Until.findObject(By.desc("dial")),
+                mABvtHelper.LONG_TIMEOUT);
+        Assert.assertNotNull("Can't reach emergency call page", dialButton);
+        mDevice.pressBack();
+        Thread.sleep(mABvtHelper.LONG_TIMEOUT);
+    }
+
+    private void removeScreenLock(String pwd) throws Exception {
+        navigateToScreenLock();
+        UiObject2 pinField = mDevice.wait(Until.findObject(By.clazz(EDIT_TEXT_CLASS_NAME)),
+                mABvtHelper.LONG_TIMEOUT);
+        pinField.setText(pwd);
+        mDevice.pressEnter();
+        mDevice.wait(Until.findObject(By.text("Swipe")), mABvtHelper.LONG_TIMEOUT).click();
+        mDevice.wait(Until.findObject(By.text("YES, REMOVE")), mABvtHelper.LONG_TIMEOUT).click();
+    }
+
+    private void unlockScreen(String pwd) throws Exception {
+        swipeUp();
+        Thread.sleep(mABvtHelper.SHORT_TIMEOUT);
+        // enter password to unlock screen
+        String command = String.format(" %s %s %s", "input", "text", pwd);
+        mDevice.executeShellCommand(command);
+        mDevice.waitForIdle();
+        Thread.sleep(mABvtHelper.SHORT_TIMEOUT);
+        mDevice.pressEnter();
+    }
+
+    private void navigateToScreenLock() throws Exception {
+        launchSettingsPage(mContext, Settings.ACTION_SECURITY_SETTINGS);
+        mDevice.wait(Until.findObject(By.text("Screen lock")), mABvtHelper.LONG_TIMEOUT).click();
+    }
+
+    private 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(mABvtHelper.LONG_TIMEOUT * 2);
+    }
+
+    private void sleepAndWakeUpDevice() throws RemoteException, InterruptedException {
+        mDevice.sleep();
+        Thread.sleep(mABvtHelper.LONG_TIMEOUT);
+        mDevice.wakeUp();
+    }
+
+    private void swipeUp() throws Exception {
+        mDevice.swipe(mDevice.getDisplayWidth() / 2, mDevice.getDisplayHeight(),
+                mDevice.getDisplayWidth() / 2, 0, 30);
+        Thread.sleep(mABvtHelper.SHORT_TIMEOUT);
+    }
+
+    private boolean isLockScreenEnabled() {
+        KeyguardManager km = (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE);
+        return km.isKeyguardSecure();
+    }
+}
+
diff --git a/tests/androidbvt/src/com/android/androidbvt/SysUIMultiUserTests.java b/tests/androidbvt/src/com/android/androidbvt/SysUIMultiUserTests.java
new file mode 100644
index 0000000..8da0c27
--- /dev/null
+++ b/tests/androidbvt/src/com/android/androidbvt/SysUIMultiUserTests.java
@@ -0,0 +1,126 @@
+/*
+ * 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.androidbvt;
+
+import android.app.UiAutomation;
+import android.content.Context;
+import android.os.Environment;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.uiautomator.UiDevice;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+
+import junit.framework.TestCase;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class SysUIMultiUserTests extends TestCase {
+    private UiAutomation mUiAutomation = null;
+    private UiDevice mDevice;
+    private Context mContext = null;
+    private AndroidBvtHelper mABvtHelper = null;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        mDevice.setOrientationNatural();
+        mContext = InstrumentationRegistry.getTargetContext();
+        mUiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        mABvtHelper = AndroidBvtHelper.getInstance(mDevice, mContext, mUiAutomation);
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        mDevice.unfreezeRotation();
+        super.tearDown();
+    }
+
+    /**
+     * Following test creates a second user and verifies user created Also ensures owner has no
+     * access to second user's dir
+     */
+    @LargeTest
+    public void testMultiUserCreate() throws InterruptedException, IOException {
+        int secondUserId = -1;
+        List<String> cmdOut;
+        try {
+            // Ensure there are exactly 1 user
+            cmdOut = mABvtHelper.executeShellCommand("pm list users");
+            assertTrue("There aren't exatcly 1 user", (cmdOut.size() - 1) == 1);
+
+            // Create user
+            cmdOut = mABvtHelper.executeShellCommand("pm create-user test");
+            assertTrue("Output should have 1 line", cmdOut.size() == 1);
+            // output format : Success: created user id 10
+            // Find user id from output above
+            Pattern pattern = Pattern.compile(
+                    "(.*)(:)(.*?)(\\d+)");
+            Matcher matcher = pattern.matcher(cmdOut.get(0));
+            if (matcher.find()) {
+                Log.i(mABvtHelper.TEST_TAG, String.format("User Name:%s UserId:%d",
+                        matcher.group(1), Integer.parseInt(matcher.group(4))));
+                secondUserId = Integer.parseInt(matcher.group(4));
+            }
+            Thread.sleep(mABvtHelper.SHORT_TIMEOUT);
+
+            // Verify second user id is created
+            cmdOut = mABvtHelper.executeShellCommand("pm list users");
+            // Sample output of "pm list users"
+            // Users:
+            // UserInfo{0:Owner:13} running
+            // UserInfo{18:test:0}
+            assertTrue("There aren't exatcly 2 users", (cmdOut.size() - 1) == 2);
+            // Get Second user id from 'list users' cmd
+            // Ensure that matches with previously created user id
+            pattern = Pattern.compile(
+                    "(.*\\{)(\\d+)(:)(.*?)(:)(\\d+)(\\}.*)"); // 2 = id 6 = flag
+            matcher = pattern.matcher(cmdOut.get(2));
+            if (matcher.find()) {
+                assertTrue("Second User id doesn't match",
+                        Integer.parseInt(matcher.group(2)) == secondUserId);
+            }
+            Thread.sleep(mABvtHelper.SHORT_TIMEOUT);
+
+            // Ensure owner has no access to second user's directory
+            final File myPath = Environment.getExternalStorageDirectory();
+            final int myId = android.os.Process.myUid() / 100000;
+            assertEquals(String.valueOf(myId), myPath.getName());
+
+            Log.i(mABvtHelper.TEST_TAG, "My path is " + myPath);
+            final File basePath = myPath.getParentFile();
+            for (int i = 0; i < 128; i++) {
+                if (i == myId) {
+                    continue;
+                }
+
+                final File otherPath = new File(basePath, String.valueOf(i));
+                assertNull("Owner have access to other user's resources!", otherPath.list());
+                assertFalse("Owner can read other user's content!", otherPath.canRead());
+            }
+        } finally {
+            cmdOut = mABvtHelper.executeShellCommand("pm remove-user " + secondUserId);
+            Log.i(mABvtHelper.TEST_TAG,
+                    String.format("Second user has been removed? %s. CmdOut = %s",
+                            cmdOut.get(0).equals("Success: removed user"), cmdOut.get(0)));
+        }
+    }
+}
diff --git a/tests/androidbvt/src/com/android/androidbvt/SysUIMultiWindowTests.java b/tests/androidbvt/src/com/android/androidbvt/SysUIMultiWindowTests.java
new file mode 100644
index 0000000..3c3886c
--- /dev/null
+++ b/tests/androidbvt/src/com/android/androidbvt/SysUIMultiWindowTests.java
@@ -0,0 +1,115 @@
+/*
+ * 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.androidbvt;
+
+import android.app.UiAutomation;
+import android.content.Context;
+import android.content.Intent;
+import android.os.RemoteException;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.uiautomator.UiDevice;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.view.accessibility.AccessibilityWindowInfo;
+
+import junit.framework.TestCase;
+
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import android.util.Log;
+public class SysUIMultiWindowTests extends TestCase {
+    private static final String CALCULATOR_PACKAGE = "com.google.android.calculator";
+    private static final String CALCULATOR_ACTIVITY = "com.android.calculator2.Calculator";
+    private static final String SETTINGS_PACKAGE = "com.android.settings";
+    private static final int FULLSCREEN = 1;
+    private static final int SPLITSCREEN = 3;
+    private UiAutomation mUiAutomation = null;
+    private UiDevice mDevice;
+    private Context mContext = null;
+    private AndroidBvtHelper mABvtHelper = null;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        mDevice.setOrientationNatural();
+        mContext = InstrumentationRegistry.getTargetContext();
+        mUiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        mABvtHelper = AndroidBvtHelper.getInstance(mDevice, mContext, mUiAutomation);
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        mDevice.unfreezeRotation();
+        super.tearDown();
+    }
+
+    /**
+     * Following test ensures any app can be docked from full-screen to split-screen, another can be
+     * launched to multiwindow mode and finally, initial app can be brought back to full-screen
+     */
+    @LargeTest
+    public void testLaunchInMultiwindow() throws InterruptedException, RemoteException {
+        // Launch calculator in full screen
+        Intent launchIntent = mContext.getPackageManager()
+                .getLaunchIntentForPackage(CALCULATOR_PACKAGE);
+        mContext.startActivity(launchIntent);
+        Thread.sleep(mABvtHelper.SHORT_TIMEOUT);
+        int taskId = -1;
+        // Find task id for launched Calculator package
+        List<String> cmdOut = mABvtHelper.executeShellCommand("am stack list");
+        for (String line : cmdOut) {
+            Pattern pattern = Pattern.compile(String.format(".*taskId=([0-9]+): %s/%s.*",CALCULATOR_PACKAGE, CALCULATOR_ACTIVITY));
+            Matcher matcher = pattern.matcher(line);
+            if (matcher.find()) {
+                taskId = Integer.parseInt(matcher.group(1));
+                break;
+            }
+        }
+        assertTrue("Taskid hasn't been found", taskId != -1);
+        // Convert calculator to multiwindow mode
+        mUiAutomation.executeShellCommand(
+                String.format("am stack movetask %d %d true", taskId, SPLITSCREEN));
+        Thread.sleep(mABvtHelper.SHORT_TIMEOUT * 2);
+        // Launch Settings
+        launchIntent = mContext.getPackageManager().getLaunchIntentForPackage(SETTINGS_PACKAGE);
+        mContext.startActivity(launchIntent);
+        Thread.sleep(mABvtHelper.SHORT_TIMEOUT * 2);
+        // Ensure settings is active window
+        List<AccessibilityWindowInfo> windows = mUiAutomation.getWindows();
+        AccessibilityWindowInfo window = windows.get(windows.size() - 1);
+        assertTrue("Settings isn't active window",
+                window.getRoot().getPackageName().equals(SETTINGS_PACKAGE));
+        // Calculate midpoint for Calculator window and click
+        mDevice.click(mDevice.getDisplayHeight() / 4, mDevice.getDisplayWidth() / 2);
+        Thread.sleep(mABvtHelper.SHORT_TIMEOUT);
+        windows = mUiAutomation.getWindows();
+        window = windows.get(windows.size() - 2);
+        assertTrue("Calcualtor isn't active window",
+                window.getRoot().getPackageName().equals(CALCULATOR_PACKAGE));
+        // Make Calculator FullWindow again and ensure Settings package isn't found on window
+        mUiAutomation.executeShellCommand(
+                String.format("am stack movetask %d %d true", taskId, FULLSCREEN));
+        Thread.sleep(mABvtHelper.SHORT_TIMEOUT * 2);
+        windows = mUiAutomation.getWindows();
+        for(int i = 0; i < windows.size() && windows.get(i).getRoot() != null; ++i) {
+            assertFalse("Settings have been found",
+                    windows.get(i).getRoot().getPackageName().equals(SETTINGS_PACKAGE));
+        }
+        mDevice.pressHome();
+    }
+}
diff --git a/tests/androidbvt/src/com/android/androidbvt/SysUINotificationShadeTests.java b/tests/androidbvt/src/com/android/androidbvt/SysUINotificationShadeTests.java
new file mode 100644
index 0000000..337ef37
--- /dev/null
+++ b/tests/androidbvt/src/com/android/androidbvt/SysUINotificationShadeTests.java
@@ -0,0 +1,316 @@
+/*
+ * 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.androidbvt;
+
+import android.app.IntentService;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.RemoteInput;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.provider.Settings;
+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.support.test.uiautomator.Until;
+import android.view.inputmethod.InputMethodManager;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.widget.EditText;
+import android.widget.Toast;
+
+import android.util.Log;
+import junit.framework.Assert;
+import junit.framework.TestCase;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class SysUINotificationShadeTests extends TestCase {
+    private static final String LOG_TAG = SysUINotificationShadeTests.class.getSimpleName();
+    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 NOTIFICATION_ID_2 = 2;
+    private static final String KEY_QUICK_REPLY_TEXT = "quick_reply";
+    private static final String INLINE_REPLY_TITLE = "INLINE REPLY TITLE";
+    private static final String RECEIVER_PKG_NAME = "com.android.systemui";
+    private static final String BUNDLE_GROUP_KEY = "group key ";
+    private UiDevice mDevice = null;
+    private Context mContext;
+    private NotificationManager mNotificationManager;
+    private ContentResolver mResolver;
+
+    private enum QuickSettingTiles {
+        WIFI("Wi-Fi"), SIM("SIM"), DND("Do not disturb"), BATTERY("Battery"),
+        FLASHLIGHT("Flashlight"), SCREEN("screen"), BLUETOOTH("Bluetooth"),
+        AIRPLANE("Airplane mode"), LOCATION("Location");
+
+        private final String name;
+
+        private QuickSettingTiles(String name) {
+            this.name = name;
+        }
+
+        public String getName() {
+            return this.name;
+        }
+    };
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        mContext = InstrumentationRegistry.getTargetContext();
+        mResolver = mContext.getContentResolver();
+        mDevice.setOrientationNatural();
+        mNotificationManager = (NotificationManager) mContext
+                .getSystemService(Context.NOTIFICATION_SERVICE);
+        mDevice.pressHome();
+        mNotificationManager.cancelAll();
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        mNotificationManager.cancelAll();
+        mDevice.pressHome();
+        mDevice.unfreezeRotation();
+        mDevice.waitForIdle();
+        super.tearDown();
+    }
+
+    /**
+     * Following test will create notifications, and verify notification can be expanded and
+     * redacted
+     */
+    @LargeTest
+    public void testNotifications() throws Exception {
+        // test receive notification and expand/redact notification
+        verifyReceiveAndExpandRedactNotification();
+        // test inline notification and dismiss notification
+        verifyInlineAndDimissNotification();
+    }
+
+    /**
+     * Following test will open Quick Setting shade, and verify icons in the shade
+     */
+    @MediumTest
+    public void testQuickSettings() throws Exception {
+        mDevice.openQuickSettings();
+        Thread.sleep(LONG_TIMEOUT * 2);
+        // Verify quick settings are displayed on the phone screen.
+        for (QuickSettingTiles tile : QuickSettingTiles.values()) {
+            UiObject2 quickSettingTile = mDevice.wait(
+                    Until.findObject(By.descContains(tile.getName())),
+                    SHORT_TIMEOUT);
+            assertNotNull(String.format("%s did not load correctly", tile.getName()),
+                    quickSettingTile);
+        }
+        // Verify tapping on Settings icon in Quick settings launches Settings.
+        mDevice.wait(Until.findObject(By.descContains("Open settings.")), LONG_TIMEOUT)
+                .click();
+        UiObject2 settingHeading = mDevice.wait(Until.findObject(By.text("Settings")),
+                LONG_TIMEOUT);
+        assertNotNull("Setting menu has not loaded correctly", settingHeading);
+    }
+
+    private void verifyReceiveAndExpandRedactNotification() throws Exception {
+        List<Integer> lists = new ArrayList<Integer>(Arrays.asList(GROUP_NOTIFICATION_ID,
+                CHILD_NOTIFICATION_ID, SECOND_CHILD_NOTIFICATION_ID));
+        sendBundlingNotifications(lists, BUNDLE_GROUP_KEY);
+        Thread.sleep(LONG_TIMEOUT);
+        swipeDown();
+        UiObject2 obj = mDevice.wait(
+                Until.findObject(By.textContains(lists.get(1).toString())),
+                LONG_TIMEOUT);
+        int currentY = obj.getVisibleCenter().y;
+        mDevice.wait(Until.findObject(By.res("android:id/expand_button")), LONG_TIMEOUT * 2)
+                .click();
+        obj = mDevice.wait(Until.findObject(By.textContains(lists.get(0).toString())),
+                LONG_TIMEOUT);
+        assertFalse("The notifications has not been bundled",
+                obj.getVisibleCenter().y == currentY);
+        mDevice.wait(Until.findObject(By.res("android:id/expand_button")), LONG_TIMEOUT).click();
+        obj = mDevice.wait(Until.findObject(By.textContains(lists.get(1).toString())),
+                LONG_TIMEOUT);
+        assertTrue("The notifications can not be redacted",
+                obj.getVisibleCenter().y == currentY);
+        mNotificationManager.cancelAll();
+    }
+
+    private void verifyInlineAndDimissNotification() throws Exception {
+        sendNotificationsWithInLineReply(NOTIFICATION_ID_2, INLINE_REPLY_TITLE);
+        Thread.sleep(LONG_TIMEOUT);
+        mDevice.openNotification();
+        mDevice.wait(Until.findObject(By.text("REPLY")), LONG_TIMEOUT).click();
+        UiObject2 replyBox = mDevice.wait(
+                Until.findObject(By.res(RECEIVER_PKG_NAME, "remote_input_send")),
+                LONG_TIMEOUT);
+        InputMethodManager imm = (InputMethodManager) mContext
+                .getSystemService(Context.INPUT_METHOD_SERVICE);
+        if (!imm.isAcceptingText()) {
+            assertNotNull("Keyboard for inline reply has not loaded correctly", replyBox);
+        }
+        UiObject2 obj = mDevice.wait(Until.findObject(By.text(INLINE_REPLY_TITLE)),
+                LONG_TIMEOUT);
+        obj.swipe(Direction.LEFT, 1.0f);
+        Thread.sleep(LONG_TIMEOUT);
+        if (checkNotificationExistence(NOTIFICATION_ID_2)) {
+            fail(String.format("Notification %s has not been dismissed", NOTIFICATION_ID_2));
+        }
+    }
+
+    /**
+     * 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
+     */
+    private void sendBundlingNotifications(List<Integer> lists, String groupKey) throws Exception {
+        Notification childNotification = new Notification.Builder(mContext)
+                .setContentTitle(lists.get(1).toString())
+                .setSmallIcon(R.drawable.stat_notify_email)
+                .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(R.drawable.stat_notify_email)
+                .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(R.drawable.stat_notify_email)
+                .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
+     */
+    private void sendNotificationsWithInLineReply(int notificationId, String title) {
+        Notification.Action action = new Notification.Action.Builder(
+                R.drawable.stat_notify_email, "Reply", ToastService.getPendingIntent(mContext,
+                        title))
+                                .addRemoteInput(new RemoteInput.Builder(KEY_QUICK_REPLY_TEXT)
+                                        .setLabel("Quick reply").build())
+                                .build();
+        Notification.Builder n = new Notification.Builder(mContext)
+                .setContentTitle(Integer.toString(notificationId))
+                .setContentText(title)
+                .setWhen(System.currentTimeMillis())
+                .setSmallIcon(R.drawable.stat_notify_email)
+                .addAction(action)
+                .setPriority(Notification.PRIORITY_HIGH)
+                .setDefaults(Notification.DEFAULT_VIBRATE);
+        mNotificationManager.notify(notificationId, n.build());
+    }
+
+    private boolean checkNotificationExistence(int id) 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;
+    }
+
+    private void swipeDown() throws Exception {
+        mDevice.swipe(mDevice.getDisplayWidth() / 2, 0, mDevice.getDisplayWidth() / 2,
+                mDevice.getDisplayHeight() / 2 + 50, 20);
+        Thread.sleep(SHORT_TIMEOUT);
+    }
+
+    public static class ToastService extends IntentService {
+        private static final String TAG = "ToastService";
+        private static final String ACTION_TOAST = "toast";
+        private Handler handler;
+
+        public ToastService() {
+            super(TAG);
+        }
+
+        public ToastService(String name) {
+            super(name);
+        }
+
+        @Override
+        public int onStartCommand(Intent intent, int flags, int startId) {
+            handler = new Handler();
+            return super.onStartCommand(intent, flags, startId);
+        }
+
+        @Override
+        protected void onHandleIntent(Intent intent) {
+            if (intent.hasExtra("text")) {
+                final String text = intent.getStringExtra("text");
+                handler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        Toast.makeText(ToastService.this, text, Toast.LENGTH_LONG).show();
+                        Log.v(TAG, "toast " + text);
+                    }
+                });
+            }
+        }
+
+        public static PendingIntent getPendingIntent(Context context, String text) {
+            Intent toastIntent = new Intent(context, ToastService.class);
+            toastIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            toastIntent.setAction(ACTION_TOAST + ":" + text); // one per toast message
+            toastIntent.putExtra("text", text);
+            PendingIntent pi = PendingIntent.getService(
+                    context, 58, toastIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+            return pi;
+        }
+    }
+}
diff --git a/tests/androidbvt/src/com/android/androidbvt/app/MediaPlaybackTestApp.java b/tests/androidbvt/src/com/android/androidbvt/app/MediaPlaybackTestApp.java
new file mode 100644
index 0000000..bfb239b
--- /dev/null
+++ b/tests/androidbvt/src/com/android/androidbvt/app/MediaPlaybackTestApp.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 com.android.androidbvt.app;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+
+import com.android.androidbvt.R;
+
+public class MediaPlaybackTestApp extends Activity {
+
+    private SurfaceView mSurfaceView;
+
+    @Override
+    public void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+        setContentView(R.layout.surface_view);
+        mSurfaceView = (SurfaceView)findViewById(R.id.surface_view);
+    }
+
+    public SurfaceHolder getSurfaceHolder() {
+        return mSurfaceView.getHolder();
+    }
+}
diff --git a/tests/camera/aupt-profile/Android.mk b/tests/camera/aupt-profile/Android.mk
new file mode 100644
index 0000000..7ba85d6
--- /dev/null
+++ b/tests/camera/aupt-profile/Android.mk
@@ -0,0 +1,25 @@
+#
+# 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.
+#
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := camera-profile-test
+LOCAL_JAVA_LIBRARIES := android.test.runner ub-uiautomator AuptLib
+LOCAL_STATIC_JAVA_LIBRARIES := app-helpers
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_MODULE_PATH := $(TARGET_OUT_DATA)/local/tmp
+
+include $(BUILD_JAVA_LIBRARY)
diff --git a/tests/camera/aupt-profile/src/com/android/test/uiautomator/aupt/camera/CameraStress4KTest.java b/tests/camera/aupt-profile/src/com/android/test/uiautomator/aupt/camera/CameraStress4KTest.java
new file mode 100644
index 0000000..a58b773
--- /dev/null
+++ b/tests/camera/aupt-profile/src/com/android/test/uiautomator/aupt/camera/CameraStress4KTest.java
@@ -0,0 +1,57 @@
+/*
+ * 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.test.uiautomator.aupt.camera;
+
+import android.support.test.aupt.AuptTestCase;
+import android.platform.test.helpers.GoogleCameraHelperImpl;
+
+/**
+ * Tests for the camera
+ */
+public class CameraStress4KTest extends AuptTestCase {
+    private GoogleCameraHelperImpl mHelper;
+    private int videoTimeMS = 5 * 1000;
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mHelper = new GoogleCameraHelperImpl(getInstrumentation());
+        if (getParams().containsKey("video-duration")) {
+            videoTimeMS = Integer.parseInt(getParams().getString("video-duration"));
+        }
+        mHelper.open();
+    }
+
+    public void testCameraStressVideoBack4K() {
+        mHelper.goToBackCamera();
+        mHelper.goToVideoMode();
+        mHelper.set4KMode(GoogleCameraHelperImpl.VIDEO_4K_MODE_ON);
+        mHelper.captureVideo(videoTimeMS);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void tearDown() throws Exception {
+        mHelper.exit();
+        super.tearDown();
+    }
+}
diff --git a/tests/camera/aupt-profile/src/com/android/test/uiautomator/aupt/camera/CameraStressHDRTest.java b/tests/camera/aupt-profile/src/com/android/test/uiautomator/aupt/camera/CameraStressHDRTest.java
new file mode 100644
index 0000000..01107b5
--- /dev/null
+++ b/tests/camera/aupt-profile/src/com/android/test/uiautomator/aupt/camera/CameraStressHDRTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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.test.uiautomator.aupt.camera;
+
+import android.support.test.aupt.AuptTestCase;
+import android.platform.test.helpers.GoogleCameraHelperImpl;
+
+/**
+ * Tests for the camera
+ */
+public class CameraStressHDRTest extends AuptTestCase {
+    private GoogleCameraHelperImpl mHelper;
+    private int videoTimeMS = 5 * 1000;
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mHelper = new GoogleCameraHelperImpl(getInstrumentation());
+        if (getParams().containsKey("video-duration")) {
+            videoTimeMS = Integer.parseInt(getParams().getString("video-duration"));
+        }
+        mHelper.open();
+    }
+
+    public void testCameraStressStillCaptureBackHDR() {
+        mHelper.goToBackCamera();
+        mHelper.goToCameraMode();
+        mHelper.setHdrMode(GoogleCameraHelperImpl.HDR_MODE_ON);
+        mHelper.capturePhoto();
+
+    }
+
+    public void testCameraStressStillCaptureFrontHDR() {
+        mHelper.goToFrontCamera();
+        mHelper.goToCameraMode();
+        mHelper.setHdrMode(GoogleCameraHelperImpl.HDR_MODE_ON);
+        mHelper.capturePhoto();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void tearDown() throws Exception {
+        mHelper.exit();
+        super.tearDown();
+    }
+}
diff --git a/tests/camera/aupt-profile/src/com/android/test/uiautomator/aupt/camera/CameraStressHFRTest.java b/tests/camera/aupt-profile/src/com/android/test/uiautomator/aupt/camera/CameraStressHFRTest.java
new file mode 100644
index 0000000..a39fb1f
--- /dev/null
+++ b/tests/camera/aupt-profile/src/com/android/test/uiautomator/aupt/camera/CameraStressHFRTest.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 com.android.test.uiautomator.aupt.camera;
+
+import android.support.test.aupt.AuptTestCase;
+import android.platform.test.helpers.GoogleCameraHelperImpl;
+
+/**
+ * Tests for the camera
+ */
+public class CameraStressHFRTest extends AuptTestCase {
+    private GoogleCameraHelperImpl mHelper;
+    private int videoTimeMS = 5 * 1000;
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mHelper = new GoogleCameraHelperImpl(getInstrumentation());
+        if (getParams().containsKey("video-duration")) {
+            videoTimeMS = Integer.parseInt(getParams().getString("video-duration"));
+        }
+        mHelper.open();
+    }
+
+    public void testCameraStressVideoBackHFR120FPS() {
+        mHelper.goToBackCamera();
+        mHelper.goToVideoMode();
+        mHelper.setHFRMode(GoogleCameraHelperImpl.HFR_MODE_120_FPS);
+        mHelper.captureVideo(videoTimeMS);
+        mHelper.setHFRMode(GoogleCameraHelperImpl.HFR_MODE_OFF);
+    }
+
+    public void testCameraStressVideoBackHFR240FPS() {
+        mHelper.goToBackCamera();
+        mHelper.goToVideoMode();
+        mHelper.setHFRMode(GoogleCameraHelperImpl.HFR_MODE_240_FPS);
+        mHelper.captureVideo(videoTimeMS);
+        mHelper.setHFRMode(GoogleCameraHelperImpl.HFR_MODE_OFF);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void tearDown() throws Exception {
+        mHelper.exit();
+        super.tearDown();
+    }
+}
diff --git a/tests/camera/aupt-profile/src/com/android/test/uiautomator/aupt/camera/CameraStressSnapshotTest.java b/tests/camera/aupt-profile/src/com/android/test/uiautomator/aupt/camera/CameraStressSnapshotTest.java
new file mode 100644
index 0000000..7c40ae6
--- /dev/null
+++ b/tests/camera/aupt-profile/src/com/android/test/uiautomator/aupt/camera/CameraStressSnapshotTest.java
@@ -0,0 +1,63 @@
+/*
+ * 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.test.uiautomator.aupt.camera;
+
+import android.support.test.aupt.AuptTestCase;
+import android.platform.test.helpers.GoogleCameraHelperImpl;
+
+/**
+ * Tests for the camera
+ */
+public class CameraStressSnapshotTest extends AuptTestCase {
+    private GoogleCameraHelperImpl mHelper;
+    private int videoTimeMs = 5 * 1000;
+    private int snapshotStartTimeMs = 1 * 1000;
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mHelper = new GoogleCameraHelperImpl(getInstrumentation());
+        if (getParams().containsKey("video-duration")) {
+            videoTimeMs = Integer.parseInt(getParams().getString("video-duration"));
+        }
+        mHelper.open();
+    }
+
+    public void testCameraStressVideoBackSnapshot() {
+        mHelper.goToBackCamera();
+        mHelper.goToVideoMode();
+        mHelper.snapshotVideo(videoTimeMs, snapshotStartTimeMs);
+    }
+
+    public void testCameraStressVideoFrontSnapshot() {
+        mHelper.goToFrontCamera();
+        mHelper.goToVideoMode();
+        mHelper.snapshotVideo(videoTimeMs, snapshotStartTimeMs);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void tearDown() throws Exception {
+        mHelper.exit();
+        super.tearDown();
+    }
+}
diff --git a/tests/camera/aupt-profile/src/com/android/test/uiautomator/aupt/camera/CameraStressTest.java b/tests/camera/aupt-profile/src/com/android/test/uiautomator/aupt/camera/CameraStressTest.java
new file mode 100644
index 0000000..effb38d
--- /dev/null
+++ b/tests/camera/aupt-profile/src/com/android/test/uiautomator/aupt/camera/CameraStressTest.java
@@ -0,0 +1,80 @@
+/*
+ * 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.test.uiautomator.aupt.camera;
+
+import android.support.test.aupt.AuptTestCase;
+import android.platform.test.helpers.GoogleCameraHelperImpl;
+
+/**
+ * Tests for the camera
+ */
+public class CameraStressTest extends AuptTestCase {
+    private GoogleCameraHelperImpl mHelper;
+    private int videoTimeMS = 5 * 1000;
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mHelper = new GoogleCameraHelperImpl(getInstrumentation());
+        if (getParams().containsKey("video-duration")) {
+            videoTimeMS = Integer.parseInt(getParams().getString("video-duration"));
+        }
+        mHelper.open();
+    }
+
+    public void testCameraStressStillCaptureBack() {
+        mHelper.goToBackCamera();
+        mHelper.goToCameraMode();
+        mHelper.setHdrMode(GoogleCameraHelperImpl.HDR_MODE_OFF);
+        mHelper.capturePhoto();
+    }
+
+    public void testCameraStressStillCaptureFront() {
+        mHelper.goToFrontCamera();
+        mHelper.goToCameraMode();
+        mHelper.setHdrMode(GoogleCameraHelperImpl.HDR_MODE_OFF);
+        mHelper.capturePhoto();
+    }
+
+    public void testCameraStressVideoBasicBack() {
+        mHelper.goToBackCamera();
+        mHelper.goToVideoMode();
+        //TODO: enable this once b/28723710 is fixed.
+        //mHelper.set4KMode(GoogleCameraHelperImpl.VIDEO_HD_1080);
+        mHelper.captureVideo(videoTimeMS);
+    }
+
+    public void testCameraStressVideoBasicFront() {
+        mHelper.goToFrontCamera();
+        mHelper.goToVideoMode();
+        //TODO: enable this once b/28723710 is fixed
+        //mHelper.set4KMode(GoogleCameraHelperImpl.VIDEO_HD_1080);
+        mHelper.captureVideo(videoTimeMS);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void tearDown() throws Exception {
+        mHelper.exit();
+        super.tearDown();
+    }
+}
diff --git a/tests/functional/applinktests/Android.mk b/tests/functional/applinktests/Android.mk
new file mode 100644
index 0000000..6644ac9
--- /dev/null
+++ b/tests/functional/applinktests/Android.mk
@@ -0,0 +1,29 @@
+# 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
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT 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_TAGS := tests
+
+LOCAL_SDK_VERSION := current
+
+LOCAL_SRC_FILES := $(call all-subdir-java-files)
+LOCAL_JAVA_LIBRARIES := android.test.runner
+LOCAL_STATIC_JAVA_LIBRARIES := launcher-helper-lib ub-uiautomator
+
+LOCAL_PACKAGE_NAME := AppLinkFunctionalTests
+LOCAL_CERTIFICATE := platform
+
+include $(BUILD_PACKAGE)
diff --git a/tests/functional/applinktests/AndroidManifest.xml b/tests/functional/applinktests/AndroidManifest.xml
new file mode 100644
index 0000000..a277696
--- /dev/null
+++ b/tests/functional/applinktests/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 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.functional.applinktests" >
+
+    <uses-sdk android:minSdkVersion="19"
+              android:targetSdkVersion="24" />
+    <application>
+        <uses-library android:name="android.test.runner"/>
+    </application>
+    <instrumentation
+            android:name="android.test.InstrumentationTestRunner"
+            android:targetPackage="com.android.functional.applinktests"
+            android:label="AppLink Functional Tests" />
+</manifest>
diff --git a/tests/functional/applinktests/src/com/android/functional/applinktests/AppLinkTests.java b/tests/functional/applinktests/src/com/android/functional/applinktests/AppLinkTests.java
new file mode 100644
index 0000000..7aa9141
--- /dev/null
+++ b/tests/functional/applinktests/src/com/android/functional/applinktests/AppLinkTests.java
@@ -0,0 +1,262 @@
+/*
+ * 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.applinktests;
+
+import android.app.UiAutomation;
+import android.content.Context;
+import android.content.Intent;
+import android.os.ParcelFileDescriptor;
+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.test.InstrumentationTestCase;
+import android.util.Log;
+import android.view.accessibility.AccessibilityWindowInfo;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.List;
+
+public class AppLinkTests extends InstrumentationTestCase {
+    public final String TEST_TAG = "AppLinkFunctionalTest";
+    public final String TEST_PKG_NAME = "com.android.applinktestapp";
+    public final String TEST_APP_NAME = "AppLinkTestApp";
+    public final String YOUTUBE_PKG_NAME = "com.google.android.youtube";
+    public final String HTTP_SCHEME = "http";
+    public final String TEST_HOST = "test.com";
+    public final int TIMEOUT = 1000;
+    private UiDevice mDevice = null;
+    private Context mContext = null;
+    private UiAutomation mUiAutomation = null;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mDevice = UiDevice.getInstance(getInstrumentation());
+        mContext = getInstrumentation().getContext();
+        mUiAutomation = getInstrumentation().getUiAutomation();
+        mDevice.setOrientationNatural();
+    }
+
+    // Ensures that default app link setting set to 'undefined' for 3P apps
+    public void testDefaultAppLinkSettting() throws InterruptedException {
+        String out = getAppLink(TEST_PKG_NAME);
+        assertTrue("Default app link not set to 'undefined' mode", "undefined".equals(out));
+        openLink(HTTP_SCHEME, TEST_HOST);
+        ensureDisambigPresent();
+    }
+
+    // User sets an app to open for a link 'Just Once' and disambig shows up next time too
+    // Once user set to 'always' disambig never shows up
+    public void testUserSetToJustOnceAndAlways() throws InterruptedException {
+        openLink(HTTP_SCHEME, TEST_HOST);
+        ensureDisambigPresent();
+        mDevice.wait(Until.findObject(By.text("AppLinkTestApp")), TIMEOUT).click();
+        mDevice.wait(Until.findObject(By.res("android:id/button_once")), TIMEOUT).click();
+        Thread.sleep(TIMEOUT);
+        verifyForegroundAppPackage(TEST_PKG_NAME);
+        openLink(HTTP_SCHEME, TEST_HOST);
+        assertTrue("Target app isn't the default choice",
+                mDevice.wait(Until.hasObject(By.text("Open with AppLinkTestApp")), TIMEOUT));
+        mDevice.wait(Until.findObject(By.res("android:id/button_once")), TIMEOUT)
+                .clickAndWait(Until.newWindow(), TIMEOUT);
+        Thread.sleep(TIMEOUT);
+        verifyForegroundAppPackage(TEST_PKG_NAME);
+        mDevice.pressHome();
+        // Ensure it doesn't change on second attempt
+        openLink(HTTP_SCHEME, TEST_HOST);
+        // Ensure disambig is present
+        mDevice.wait(Until.findObject(By.res("android:id/button_always")), TIMEOUT).click();
+        mDevice.pressHome();
+        // User chose to set to always and intent is opened in target direct
+        openLink(HTTP_SCHEME, TEST_HOST);
+        verifyForegroundAppPackage(TEST_PKG_NAME);
+    }
+
+    // Ensure verified app always open even candidate but unverified app set to 'always'
+    public void testVerifiedAppOpenWhenNotVerifiedSetToAlways() throws InterruptedException {
+        setAppLink(TEST_PKG_NAME, "always");
+        setAppLink(YOUTUBE_PKG_NAME, "always");
+        Thread.sleep(TIMEOUT);
+        openLink(HTTP_SCHEME, "youtube.com");
+        verifyForegroundAppPackage(YOUTUBE_PKG_NAME);
+    }
+
+    // Ensure verified app always open even one candidate but unverified app set to 'ask'
+    public void testVerifiedAppOpenWhenUnverifiedSetToAsk() throws InterruptedException {
+        setAppLink(TEST_PKG_NAME, "ask");
+        setAppLink(YOUTUBE_PKG_NAME, "always");
+        String out = getAppLink(YOUTUBE_PKG_NAME);
+        openLink(HTTP_SCHEME, "youtube.com");
+        verifyForegroundAppPackage(YOUTUBE_PKG_NAME);
+    }
+
+    // Ensure disambig is shown if verified app set to 'never' and unverified app set to 'ask'
+    public void testUserChangeVerifiedLinkHandler() throws InterruptedException {
+        setAppLink(TEST_PKG_NAME, "ask");
+        setAppLink(YOUTUBE_PKG_NAME, "never");
+        Thread.sleep(TIMEOUT);
+        openLink(HTTP_SCHEME, "youtube.com");
+        ensureDisambigPresent();
+        setAppLink(YOUTUBE_PKG_NAME, "always");
+        Thread.sleep(TIMEOUT);
+        openLink(HTTP_SCHEME, "youtube.com");
+        verifyForegroundAppPackage(YOUTUBE_PKG_NAME);
+    }
+
+    // Ensure unverified app always open when unverified app set to always but verified app set to
+    // never
+    public void testTestAppSetToAlwaysVerifiedSetToNever() throws InterruptedException {
+        setAppLink(TEST_PKG_NAME, "always");
+        setAppLink(YOUTUBE_PKG_NAME, "never");
+        Thread.sleep(TIMEOUT);
+        openLink(HTTP_SCHEME, "youtube.com");
+        verifyForegroundAppPackage(TEST_PKG_NAME);
+    }
+
+    // Test user can modify 'App Link Settings'
+    public void testSettingsChangeUI() throws InterruptedException {
+        Intent intent_as = new Intent(
+                android.provider.Settings.ACTION_APPLICATION_SETTINGS);
+        mContext.startActivity(intent_as);
+        Thread.sleep(TIMEOUT * 5);
+        mDevice.wait(Until.findObject(By.res("com.android.settings:id/advanced")), TIMEOUT)
+                .clickAndWait(Until.newWindow(), TIMEOUT);
+        mDevice.wait(Until.findObject(By.text("Opening links")), TIMEOUT)
+                .clickAndWait(Until.newWindow(), TIMEOUT);
+        mDevice.wait(Until.findObject(By.text("AppLinkTestApp")), TIMEOUT)
+                .clickAndWait(Until.newWindow(), TIMEOUT);
+        mDevice.wait(Until.findObject(By.text("Open supported links")), TIMEOUT)
+                .clickAndWait(Until.newWindow(), TIMEOUT);
+        mDevice.wait(Until.findObject(By.text("Open in this app")), TIMEOUT)
+                .clickAndWait(Until.newWindow(), TIMEOUT);
+        String out = getAppLink(TEST_PKG_NAME);
+        Thread.sleep(TIMEOUT);
+        assertTrue(String.format("Default app link not set to 'always ask' rather set to %s", out),
+                "always".equals(out));
+        mDevice.wait(Until.findObject(By.text("Open supported links")), TIMEOUT)
+                .clickAndWait(Until.newWindow(), TIMEOUT);
+        mDevice.wait(Until.findObject(By.text("Don’t open in this app")), TIMEOUT)
+                .clickAndWait(Until.newWindow(), TIMEOUT);
+        out = getAppLink(TEST_PKG_NAME);
+        Thread.sleep(TIMEOUT);
+        assertTrue(String.format("Default app link not set to 'never' rather set to %s", out),
+                "never".equals(out));
+        mDevice.wait(Until.findObject(By.text("Open supported links")), TIMEOUT)
+                .clickAndWait(Until.newWindow(), TIMEOUT);
+        mDevice.wait(Until.findObject(By.text("Ask every time")), TIMEOUT)
+                .clickAndWait(Until.newWindow(), TIMEOUT);
+        out = getAppLink(TEST_PKG_NAME);
+        Thread.sleep(TIMEOUT);
+        assertTrue(String.format("Default app link not set to 'always ask' rather set to %s", out),
+                "always ask".equals(out));
+    }
+
+    // Ensure system apps that claim to open always for set to always
+    public void testSysappAppLinkSettings() {
+        // List of system app that are set to 'Always' for certain urls
+        List<String> alwaysOpenApps = new ArrayList<String>();
+        alwaysOpenApps.add("com.google.android.apps.docs.editors.docs"); // Docs
+        alwaysOpenApps.add("com.google.android.apps.docs.editors.sheets"); // Sheets
+        alwaysOpenApps.add("com.google.android.apps.docs.editors.slides"); // Slides
+        alwaysOpenApps.add("com.google.android.apps.docs"); // Drive
+        alwaysOpenApps.add("com.google.android.youtube"); // YouTube
+        for (String alwaysOpenApp : alwaysOpenApps) {
+            String out = getAppLink(alwaysOpenApp);
+            assertTrue(String.format("App link for %s should be set to 'Always'", alwaysOpenApp),
+                    "always".equalsIgnoreCase(out));
+        }
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        executeShellCommand("pm clear " + TEST_PKG_NAME);
+        executeShellCommand("pm clear " + YOUTUBE_PKG_NAME);
+        executeShellCommand("pm set-app-link " + TEST_PKG_NAME + " undefined");
+        executeShellCommand("pm set-app-link " + YOUTUBE_PKG_NAME + " always");
+        Thread.sleep(TIMEOUT);
+        mDevice.unfreezeRotation();
+        mDevice.pressHome();
+        super.tearDown();
+    }
+
+    // Start an intent to open a test link
+    private void openLink(String scheme, String host) throws InterruptedException {
+        String out = executeShellCommand(String.format(
+                "am start -a android.intent.action.VIEW -d %s://%s/", scheme, host));
+        Thread.sleep(TIMEOUT * 2);
+    }
+
+    // If framework identifies more than one app that can handle a link intent, framework presents a
+    // window to user to choose the app to handle the intent.
+    // This is also known as 'disambig' window
+    private void ensureDisambigPresent() {
+        assertNotNull("Disambig dialog is not shown",
+                mDevice.wait(Until.hasObject(By.res("android:id/resolver_list")),
+                        TIMEOUT));
+        List<UiObject2> resolverApps = mDevice.wait(Until.findObjects(By.res("android:id/text1")),
+                TIMEOUT);
+        assertTrue("There aren't exactly 2 apps to resolve", resolverApps.size() == 2);
+        assertTrue("Resolver apps aren't correct",
+                "AppLinkTestApp".equals(resolverApps.get(0).getText()) &&
+                        "Chrome".equals(resolverApps.get(1).getText()));
+    }
+
+    // Verifies that a certain package is in foreground
+    private void verifyForegroundAppPackage(String pkgName) throws InterruptedException {
+        int counter = 3;
+        List<AccessibilityWindowInfo> windows = null;
+        while (--counter > 0 && windows == null) {
+            windows = mUiAutomation.getWindows();
+            Thread.sleep(TIMEOUT);
+        }
+        assertTrue(String.format("%s is not top activity", "youtube"),
+                windows.get(windows.size() - 1).getRoot().getPackageName().equals(pkgName));
+    }
+
+    // Gets app link for a package
+    private String getAppLink(String pkgName) {
+        return executeShellCommand(String.format("pm get-app-link %s", pkgName));
+    }
+
+    // Sets Openlink settings for a package to passed value
+    private void setAppLink(String pkgName, String valueToBeSet) {
+        executeShellCommand(String.format("pm set-app-link %s %s", pkgName, valueToBeSet));
+    }
+
+    // Executes 'adb shell' command. Converts ParcelFileDescriptor output to String
+    private String executeShellCommand(String command) {
+        if (command == null || command.isEmpty()) {
+            return null;
+        }
+        ParcelFileDescriptor pfd = mUiAutomation.executeShellCommand(command);
+        try (BufferedReader reader = new BufferedReader(
+                new InputStreamReader(new FileInputStream(pfd.getFileDescriptor())))) {
+            String str = reader.readLine();
+            Log.d(TEST_TAG, String.format("Executing command: %s", command));
+            return str;
+        } catch (IOException e) {
+            Log.e(TEST_TAG, e.getMessage());
+        }
+
+        return null;
+    }
+}
diff --git a/tests/functional/appsmoke/Android.mk b/tests/functional/appsmoke/Android.mk
new file mode 100644
index 0000000..17d41c9
--- /dev/null
+++ b/tests/functional/appsmoke/Android.mk
@@ -0,0 +1,26 @@
+#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.
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_MODULE_TAGS := tests
+LOCAL_PACKAGE_NAME := AppSmoke
+
+LOCAL_STATIC_JAVA_LIBRARIES := ub-uiautomator launcher-helper-lib android-support-test
+LOCAL_CERTIFICATE := platform
+
+include $(BUILD_PACKAGE)
diff --git a/tests/functional/appsmoke/AndroidManifest.xml b/tests/functional/appsmoke/AndroidManifest.xml
new file mode 100644
index 0000000..ebd8cb5
--- /dev/null
+++ b/tests/functional/appsmoke/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<?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.
+-->
+
+<!-- package name must be unique so suffix with "tests" so package loader doesn't ignore us -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.test.appsmoke"
+    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.appsmoke"
+                     android:label="Prebuilt App Smoke Test"/>
+
+</manifest>
diff --git a/tests/functional/appsmoke/src/android/test/appsmoke/AppSmokeTest.java b/tests/functional/appsmoke/src/android/test/appsmoke/AppSmokeTest.java
new file mode 100644
index 0000000..d061304
--- /dev/null
+++ b/tests/functional/appsmoke/src/android/test/appsmoke/AppSmokeTest.java
@@ -0,0 +1,259 @@
+/*
+ * 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.appsmoke;
+
+import android.app.ActivityManagerNative;
+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.os.Bundle;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.launcherhelper.ILauncherStrategy;
+import android.support.test.launcherhelper.LauncherStrategyFactory;
+import android.support.test.uiautomator.UiDevice;
+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.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+@RunWith(Parameterized.class)
+public class AppSmokeTest {
+
+    private static final String TAG = AppSmokeTest.class.getSimpleName();
+    private static final String EXCLUDE_LIST = "exclude_apps";
+    private static final String DEBUG_LIST = "debug_apps";
+    private static final long WAIT_FOR_ANR = 6000;
+
+    @Parameter
+    public LaunchParameter mAppInfo;
+
+    private boolean mAppHasError = false;
+    private boolean mLaunchIntentDetected = false;
+    private ILauncherStrategy mLauncherStrategy = null;
+    private static UiDevice sDevice = null;
+
+    /**
+     * Convenient internal class to hold some launch specific data
+     */
+    private static class LaunchParameter implements Comparable<LaunchParameter>{
+        public String appName;
+        public String packageName;
+        public String activityName;
+
+        private LaunchParameter(String appName, String packageName, String activityName) {
+            this.appName = appName;
+            this.packageName = packageName;
+            this.activityName = activityName;
+        }
+
+        @Override
+        public int compareTo(LaunchParameter another) {
+            return appName.compareTo(another.appName);
+        }
+
+        @Override
+        public String toString() {
+            return appName;
+        }
+
+        public String toLongString() {
+            return String.format("%s [activity: %s/%s]", appName, packageName, activityName);
+        }
+    }
+
+    /**
+     * an activity controller to detect app launch crashes/ANR etc
+     */
+    private IActivityController mActivityController = new IActivityController.Stub() {
+
+        @Override
+        public int systemNotResponding(String msg) throws RemoteException {
+            // let system die
+            return -1;
+        }
+
+        @Override
+        public int appNotResponding(String processName, int pid, String processStats)
+                throws RemoteException {
+            if (processName.startsWith(mAppInfo.packageName)) {
+                mAppHasError = true;
+            }
+            // kill app
+            return -1;
+        }
+
+        @Override
+        public int appEarlyNotResponding(String processName, int pid, String annotation)
+                throws RemoteException {
+            // do nothing
+            return 0;
+        }
+
+        @Override
+        public boolean appCrashed(String processName, int pid, String shortMsg, String longMsg,
+                long timeMillis, String stackTrace) throws RemoteException {
+            if (processName.startsWith(mAppInfo.packageName)) {
+                mAppHasError = true;
+            }
+            return false;
+        }
+
+        @Override
+        public boolean activityStarting(Intent intent, String pkg) throws RemoteException {
+            Log.d(TAG, String.format("activityStarting: pkg=%s intent=%s",
+                    pkg, intent.toInsecureString()));
+            // always allow starting
+            if (pkg.equals(mAppInfo.packageName)) {
+                mLaunchIntentDetected = true;
+            }
+            return true;
+        }
+
+        @Override
+        public boolean activityResuming(String pkg) throws RemoteException {
+            Log.d(TAG, String.format("activityResuming: pkg=%s", pkg));
+            // always allow resuming
+            return true;
+        }
+    };
+
+    /**
+     * Generate the list of apps to test for launches by querying package manager
+     * @return
+     */
+    @Parameters(name = "{0}")
+    public static Collection<LaunchParameter> generateAppsList() {
+        Instrumentation instr = InstrumentationRegistry.getInstrumentation();
+        Bundle args = InstrumentationRegistry.getArguments();
+        Context ctx = instr.getTargetContext();
+        List<LaunchParameter> ret = new ArrayList<>();
+        Set<String> excludedApps = new HashSet<>();
+        Set<String> debugApps = new HashSet<>();
+
+        // parse list of app names that should be execluded from launch tests
+        if (args.containsKey(EXCLUDE_LIST)) {
+            excludedApps.addAll(Arrays.asList(args.getString(EXCLUDE_LIST).split(",")));
+        }
+        // parse list of app names used for debugging (i.e. essentially a whitelist)
+        if (args.containsKey(DEBUG_LIST)) {
+            debugApps.addAll(Arrays.asList(args.getString(DEBUG_LIST).split(",")));
+        }
+        LauncherApps la = (LauncherApps)ctx.getSystemService(Context.LAUNCHER_APPS_SERVICE);
+        UserManager um = (UserManager)ctx.getSystemService(Context.USER_SERVICE);
+        List<LauncherActivityInfo> activities = new ArrayList<>();
+        for (UserHandle handle : um.getUserProfiles()) {
+            activities.addAll(la.getActivityList(null, handle));
+        }
+        for (LauncherActivityInfo info : activities) {
+            String label = info.getLabel().toString();
+            if (!debugApps.isEmpty()) {
+                if (!debugApps.contains(label)) {
+                    // if debug apps non-empty, we are essentially in whitelist mode
+                    // bypass any apps not on list
+                    continue;
+                }
+            } else if (excludedApps.contains(label)) {
+                // if not debugging apps, bypass any excluded apps
+                continue;
+            }
+            ret.add(new LaunchParameter(label, info
+                    .getApplicationInfo().packageName, info.getName()));
+        }
+        Collections.sort(ret);
+        return ret;
+    }
+
+    @Before
+    public void before() throws RemoteException {
+        ActivityManagerNative.getDefault().setActivityController(mActivityController, false);
+        mLauncherStrategy = LauncherStrategyFactory.getInstance(sDevice).getLauncherStrategy();
+        mAppHasError = false;
+        mLaunchIntentDetected = false;
+    }
+
+    @BeforeClass
+    public static void beforeClass() throws RemoteException {
+        sDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+        sDevice.setOrientationNatural();
+    }
+
+    @After
+    public void after() throws RemoteException {
+        sDevice.pressHome();
+        ActivityManagerNative.getDefault().forceStopPackage(
+                mAppInfo.packageName, UserHandle.USER_ALL);
+        ActivityManagerNative.getDefault().setActivityController(null, false);
+    }
+
+    @AfterClass
+    public static void afterClass() throws RemoteException {
+        sDevice.unfreezeRotation();
+    }
+
+    @Test
+    public void testAppLaunch() {
+        Log.d(TAG, "Launching: " + mAppInfo.toLongString());
+        long timestamp = mLauncherStrategy.launch(mAppInfo.appName, mAppInfo.packageName);
+        boolean launchResult = (timestamp != ILauncherStrategy.LAUNCH_FAILED_TIMESTAMP);
+        if (launchResult) {
+            // poke app to check if it's responsive
+            pokeApp();
+            SystemClock.sleep(WAIT_FOR_ANR);
+        }
+        if (mAppHasError) {
+            Assert.fail("app crash or ANR detected");
+        }
+        if (!launchResult && !mLaunchIntentDetected) {
+            Assert.fail("no app crash or ANR detected, but failed to launch via UI");
+        }
+        // if launchResult is false but mLaunchIntentDetected is true, we consider it as success
+        // this happens when an app is a trampoline activity to something else
+    }
+
+    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");
+        }
+    }
+}
diff --git a/tests/functional/downloadapp/Android.mk b/tests/functional/downloadapp/Android.mk
new file mode 100644
index 0000000..814537b
--- /dev/null
+++ b/tests/functional/downloadapp/Android.mk
@@ -0,0 +1,15 @@
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_SDK_VERSION := current
+
+LOCAL_SRC_FILES := $(call all-subdir-java-files)
+LOCAL_JAVA_LIBRARIES := android.test.runner
+LOCAL_STATIC_JAVA_LIBRARIES := launcher-helper-lib ub-uiautomator android-support-test
+
+LOCAL_PACKAGE_NAME := DownloadAppFunctionalTests
+LOCAL_CERTIFICATE := platform
+
+include $(BUILD_PACKAGE)
diff --git a/tests/functional/downloadapp/AndroidManifest.xml b/tests/functional/downloadapp/AndroidManifest.xml
new file mode 100644
index 0000000..3e46794
--- /dev/null
+++ b/tests/functional/downloadapp/AndroidManifest.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 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.functional.downloadapp">
+
+    <uses-sdk android:minSdkVersion="19"
+              android:targetSdkVersion="24" />
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
+    <uses-permission android:name="android.permission.SET_TIME" />
+    <application>
+        <uses-library android:name="android.test.runner"/>
+    </application>
+    <instrumentation
+            android:name="android.support.test.runner.AndroidJUnitRunner"
+            android:targetPackage="com.android.functional.downloadapp"
+            android:label="DownloadApp Functional Tests" />
+</manifest>
diff --git a/tests/functional/downloadapp/src/com/android/functional/downloadapp/DownloadAppTestHelper.java b/tests/functional/downloadapp/src/com/android/functional/downloadapp/DownloadAppTestHelper.java
new file mode 100644
index 0000000..5427bc3
--- /dev/null
+++ b/tests/functional/downloadapp/src/com/android/functional/downloadapp/DownloadAppTestHelper.java
@@ -0,0 +1,328 @@
+/*
+ * 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.downloadapp;
+
+import android.app.AlarmManager;
+import android.app.DownloadManager;
+import android.app.DownloadManager.Query;
+import android.app.DownloadManager.Request;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.icu.util.Calendar;
+import android.net.Uri;
+import android.os.Environment;
+import android.support.test.launcherhelper.ILauncherStrategy;
+import android.support.test.launcherhelper.LauncherStrategyFactory;
+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 junit.framework.Assert;
+
+import java.util.ArrayList;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Random;
+
+public class DownloadAppTestHelper {
+    private static DownloadAppTestHelper mInstance = null;
+    public static final String[] FILE_TYPES = new String[] {
+            "pdf", "jpg", "jpeg", "doc", "xls", "txt", "rtf", "ppt", "gif", "png"
+    };
+
+    public static final String PACKAGE_NAME = "com.android.documentsui";
+    public static final String APP_NAME = "Downloads";
+    public static final String TEST_TAG = "DownloadAppTest";
+    public final int TIMEOUT = 500;
+    public final int MIN_FILENAME_LEN = 4;
+    private Context mContext = null;
+    private UiDevice mDevice = null;
+    private DownloadManager mDownloadMgr;
+    private Hashtable<String, DlObjSizeTimePair> mDownloadedItems =
+            new Hashtable<String, DlObjSizeTimePair>();
+    public ILauncherStrategy mLauncherStrategy;
+
+    private DownloadAppTestHelper(UiDevice device, Context context) {
+        mDevice = device;
+        mContext = context;
+        mLauncherStrategy = LauncherStrategyFactory.getInstance(mDevice).getLauncherStrategy();
+    }
+
+    public static DownloadAppTestHelper getInstance(UiDevice device, Context context) {
+        if (mInstance == null) {
+            mInstance = new DownloadAppTestHelper(device, context);
+        }
+        return mInstance;
+    }
+
+    public DownloadManager getDLManager() {
+        return (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE);
+    }
+
+    public void launchApp(String packageName, String appName) {
+        if (!mDevice.hasObject(By.pkg(packageName).depth(0))) {
+            mLauncherStrategy.launch(appName, packageName);
+        }
+    }
+
+    /** sort items in Download app by name, size, time */
+    public void sortByParam(String sortby) {
+        UiObject2 sortMenu = mDevice
+                .wait(Until.findObject(By.res(PACKAGE_NAME, "menu_sort")), 200);
+        if (sortMenu == null) {
+            mDevice.wait(Until.findObject(By.desc("More options")), 200).click();
+            sortMenu = mDevice.wait(Until.findObject(By.res("android:id/submenuarrow")), 200);
+        }
+        sortMenu.click();
+        mDevice.wait(Until.findObject(By.text(String.format("By %s", sortby))), 200).click();
+        mDevice.waitForIdle();
+    }
+
+    /** returns text list of items in Download app */
+    public List<String> getDownloadItemNames() {
+        List<UiObject2> itmesList = mDevice.wait(Until.findObjects(By.res("android:id/title")),
+                TIMEOUT);
+        List<String> nameList = new ArrayList<String>();
+        for (UiObject2 item : itmesList) {
+            nameList.add(item.getText());
+        }
+        return nameList;
+    }
+
+    /** verifies items in DownloadApp UI are sorted by name */
+    public Boolean verifySortedByName() {
+        List<String> nameList = getDownloadItemNames();
+        for (int i = 0; i < (nameList.size() - 1); ++i) {
+            if (nameList.get(i).compareToIgnoreCase(nameList.get(i + 1)) > 0) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /** verifies items in DownloadApp UI are sorted by size */
+    public Boolean verifySortedBySize() {
+        List<String> nameList = getDownloadItemNames();
+        for (int i = 0; i < (nameList.size() - 1); ++i) {
+            DlObjSizeTimePair firstItem = mDownloadedItems.get(nameList.get(i));
+            DlObjSizeTimePair secondItem = mDownloadedItems.get(nameList.get(i + 1));
+            if (firstItem != null && secondItem != null
+                    && firstItem.dlObjSize < secondItem.dlObjSize) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /** verifies items in DownloadApp UI are sorted by time */
+    public Boolean verifySortedByTime() {
+        List<String> nameList = getDownloadItemNames();
+        for (int i = 0; i < (nameList.size() - 1); ++i) {
+            DlObjSizeTimePair firstItem = mDownloadedItems.get(nameList.get(i));
+            DlObjSizeTimePair secondItem = mDownloadedItems.get(nameList.get(i + 1));
+            if (firstItem != null && secondItem != null
+                    && firstItem.dlObjTimeInMilliSec < secondItem.dlObjTimeInMilliSec) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public void verifyDownloadViewType(UIViewType view) {
+        int counter = 5;
+        UiObject2 viewTypeObj = null;
+        while ((viewTypeObj = mDevice.wait(
+                Until.findObject(By.res(String.format("%s:id/%s",
+                        DownloadAppTestHelper.PACKAGE_NAME,view.toString()))),200)) == null
+                        && counter-- > 0);
+        Assert.assertNotNull(viewTypeObj);
+    }
+
+    /**
+     * Create word of random assortment of lower/upper case letters
+     */
+    /** set system time to random n[0..29] days earlier */
+    public long changeSystemTime(long timeToSet) {
+        Calendar c = Calendar.getInstance();
+        c.setTimeInMillis(timeToSet);
+        AlarmManager am = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
+        am.setTime(c.getTimeInMillis());
+        return c.getTimeInMillis();
+    }
+
+    /** add some content to download DB using DownloadManager.addCompletedDownload api */
+    public void populateContentInDLApp(int count) {
+        int totalDownloaded = getTotalNumberDownloads();
+        if (totalDownloaded >= count) {
+            return;
+        }
+        Random random = new Random();
+        long currentTime = System.currentTimeMillis();
+        for (int i = 0; i < (count - totalDownloaded); ++i) {
+            String fileName = String.format("%s.%s",
+                    DownloadAppTestHelper.randomWord(random.nextInt(8) + MIN_FILENAME_LEN),
+                    DownloadAppTestHelper.FILE_TYPES[random.nextInt(FILE_TYPES.length)]);
+            int size = random.nextInt(1000);
+            // changing system time to simulate the usecase "downloaded items over a period of time"
+            long timeInMiliSec = changeSystemTime(
+                    System.currentTimeMillis() - random.nextInt(30 * 24) * (60 * 60 * 1000));
+            long dlId = -1;
+
+            dlId = getDLManager().addCompletedDownload(
+                    fileName,
+                    String.format("%s Desc",
+                            DownloadAppTestHelper.randomWord(random.nextInt(8) + MIN_FILENAME_LEN)),
+                    Boolean.FALSE,
+                    DownloadAppTestHelper.FILE_TYPES[random.nextInt(FILE_TYPES.length)],
+                    Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
+                            .getAbsolutePath(),
+                    size, Boolean.FALSE);
+            Assert.assertFalse("Add to DonwloadDB has failed!", dlId == -1);
+            Log.d(TEST_TAG, String.format("Adding Name = %s, size = %d, time = %d", fileName,
+                    size, timeInMiliSec));
+            mDownloadedItems.put(fileName, new DlObjSizeTimePair(size, timeInMiliSec));
+        }
+        changeSystemTime(currentTime);
+    }
+
+    /** add some content to download DB directly in hacky way to bypass addCompletedDownload Api*/
+    public long addToDownloadContentDB(String title, String description,
+            boolean isMediaScannerScannable, String mimeType, String path, long length,
+            boolean showNotification) {
+
+        boolean allowWrite = Boolean.FALSE;
+        Uri uri = Uri.parse("http://blah-blah"); // just put something in url format
+        Uri referer = null;
+        Request request;
+        request = new Request(uri);
+        ContentValues values = new ContentValues();
+        /**
+         * a hacky way to insert into the Download DB direct bypassing the api with minimal data
+         * Constants have been taken from
+         * /platform/frameworks/base/+/master/core/java/android/provider/Downloads.java
+         */
+        values.put("title", title);
+        values.put("description", description);
+        values.put("mimetype", mimeType);
+        values.put("is_public_api", true);
+        values.put("scanned", isMediaScannerScannable);
+        values.put("is_visible_in_downloads_ui", Boolean.TRUE);
+        values.put("destination", 6); // 6: show the download item in app
+        values.put("_data", path); // location to save the downloaded file
+        values.put("status", 200); // 200 : STATUS_SUCCESS
+        values.put("total_bytes", length);
+        values.put("visibility", (showNotification)
+                ? Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION : Request.VISIBILITY_HIDDEN);
+        Uri downloadUri = mContext.getContentResolver()
+                .insert(Uri.parse("content://downloads/my_downloads"), values);
+        if (downloadUri == null) {
+            return -1;
+        }
+        return Long.parseLong(downloadUri.getLastPathSegment());
+    }
+
+    /** remove downloads from download content db */
+    public void removeContentInDLApp() {
+        Cursor cursor = null;
+        try {
+            Query query = new Query();
+            cursor = getDLManager().query(query);
+            int columnIndex = cursor.getColumnIndex(DownloadManager.COLUMN_ID);
+            long[] removeIds = new long[cursor.getCount() - 1];
+            Log.d(TEST_TAG, String.format("Remove Size is = %d", cursor.getCount()));
+            for (int i = 0; i < (cursor.getCount() - 1); i++) {
+                cursor.moveToNext();
+                removeIds[i] = cursor.getLong(columnIndex);
+            }
+            if (removeIds.length > 0) {
+                Assert.assertEquals(removeIds.length, getDLManager().remove(removeIds));
+            }
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+    }
+
+    public int getTotalNumberDownloads() {
+        Cursor cursor = null;
+        try {
+            Query query = new Query();
+            cursor = getDLManager().query(query);
+            return cursor.getCount();
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+    }
+
+    public int getDownloadItemCountById(long[] downloadId) {
+        Cursor cursor = null;
+        int total = 0;
+        try {
+            Query query = new Query().setFilterById(downloadId);
+            cursor = getDLManager().query(query);
+            total = cursor.getCount();
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+
+        return total;
+    }
+
+    public static String randomWord(int length) {
+        Random random = new Random();
+        StringBuilder result = new StringBuilder();
+        for (int j = 0; j < length; j++) {
+            int base = random.nextInt(2) == 0 ? 'A' : 'a';
+            result.append((char) (random.nextInt(26) + base));
+        }
+        return result.toString();
+    }
+
+    /**
+     * Class to hold size and time info on downloaded items
+     */
+    class DlObjSizeTimePair {
+        int dlObjSize;
+        long dlObjTimeInMilliSec;
+
+        public DlObjSizeTimePair(int size, long time) {
+            this.dlObjSize = size;
+            this.dlObjTimeInMilliSec = time;
+        }
+    }
+
+    public enum UIViewType {
+        LIST {
+            public String toString() {
+                return "menu_list";
+            }
+        },
+        GRID {
+            public String toString() {
+                return "menu_grid";
+            }
+        }
+    };
+}
diff --git a/tests/functional/downloadapp/src/com/android/functional/downloadapp/DownloadAppTests.java b/tests/functional/downloadapp/src/com/android/functional/downloadapp/DownloadAppTests.java
new file mode 100644
index 0000000..40c3fa5
--- /dev/null
+++ b/tests/functional/downloadapp/src/com/android/functional/downloadapp/DownloadAppTests.java
@@ -0,0 +1,179 @@
+/*
+ * 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.downloadapp;
+
+import android.content.Context;
+import android.os.Environment;
+import android.os.SystemClock;
+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.MediumTest;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.test.suitebuilder.annotation.Suppress;
+import android.util.Log;
+
+import com.android.functional.downloadapp.DownloadAppTestHelper.UIViewType;
+
+import junit.framework.Assert;
+
+import java.util.Random;
+
+public class DownloadAppTests extends InstrumentationTestCase {
+    private DownloadAppTestHelper mDLAppHelper = null;
+    private UiDevice mDevice = null;
+    private Context mContext = null;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mDevice = UiDevice.getInstance(getInstrumentation());
+        mContext = getInstrumentation().getContext();
+        mDLAppHelper = DownloadAppTestHelper.getInstance(mDevice, mContext);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @SmallTest
+    public void testAddCompletedDownload() throws Exception {
+        Random random = new Random();
+        Long dlId = mDLAppHelper.addToDownloadContentDB(
+                String.format("%s.pdf", DownloadAppTestHelper.randomWord(random.nextInt(8) + 2)),
+                String.format("%s Desc", DownloadAppTestHelper.randomWord(random.nextInt(9) + 4)),
+                Boolean.FALSE,
+                DownloadAppTestHelper.FILE_TYPES[random.nextInt(
+                        DownloadAppTestHelper.FILE_TYPES.length)],
+                Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
+                        .getAbsolutePath(),
+                random.nextInt(2 * mDLAppHelper.TIMEOUT), Boolean.FALSE);
+        assertTrue("Download item <> 1",
+                1 == mDLAppHelper.getDownloadItemCountById(new long[] {
+                        dlId
+                }));
+    }
+
+    @MediumTest
+    public void testScroll() {
+        mDLAppHelper.populateContentInDLApp(20);
+        mDLAppHelper.launchApp(DownloadAppTestHelper.PACKAGE_NAME, DownloadAppTestHelper.APP_NAME);
+        UiObject2 container = mDevice.wait(Until.findObject(
+                By.res("com.android.documentsui:id/container_directory")), mDLAppHelper.TIMEOUT);
+        container.scroll(Direction.UP, 1.0f);
+        mDevice.waitForIdle();
+        container.scroll(Direction.DOWN, 1.0f);
+    }
+
+    @MediumTest
+    public void testSortByName() throws Exception {
+        Log.d(mDLAppHelper.TEST_TAG, String.format("Before sortbyname tests, total count is %d",
+                mDLAppHelper.getTotalNumberDownloads()));
+        mDLAppHelper.populateContentInDLApp(5);
+        mDLAppHelper.launchApp(DownloadAppTestHelper.PACKAGE_NAME, DownloadAppTestHelper.APP_NAME);
+        mDLAppHelper.sortByParam("name");
+        assertTrue("DL items can't be sorted by name", mDLAppHelper.verifySortedByName());
+    }
+
+    @Suppress
+    @MediumTest
+    public void testSortBySize() {
+        mDLAppHelper.populateContentInDLApp(5);
+        mDLAppHelper.launchApp(DownloadAppTestHelper.PACKAGE_NAME, DownloadAppTestHelper.APP_NAME);
+        mDLAppHelper.sortByParam("size");
+        assertTrue("DL items can't be sorted by size", mDLAppHelper.verifySortedBySize());
+    }
+
+    @MediumTest
+    public void testSortByTime() {
+        mDLAppHelper.populateContentInDLApp(5);
+        mDLAppHelper.launchApp(DownloadAppTestHelper.PACKAGE_NAME, DownloadAppTestHelper.APP_NAME);
+        mDLAppHelper.sortByParam("date modified");
+        assertTrue("DL items can't be sorted by time", mDLAppHelper.verifySortedByTime());
+    }
+
+    @MediumTest
+    public void testToggleViewTypeForDownloadItems() {
+        mDLAppHelper.populateContentInDLApp(10);
+        mDLAppHelper.launchApp(DownloadAppTestHelper.PACKAGE_NAME, DownloadAppTestHelper.APP_NAME);
+        mDevice.wait(Until.findObject(By.res(
+                String.format("%s:id/%s", DownloadAppTestHelper.PACKAGE_NAME, UIViewType.LIST))),
+                2 * mDLAppHelper.TIMEOUT).click();
+        mDLAppHelper.verifyDownloadViewType(UIViewType.GRID);
+        mDevice.wait(Until.findObject(By.res(
+                String.format("%s:id/%s", DownloadAppTestHelper.PACKAGE_NAME, UIViewType.GRID))),
+                2 * mDLAppHelper.TIMEOUT).click();
+        mDLAppHelper.verifyDownloadViewType(UIViewType.LIST);
+    }
+
+    @MediumTest
+    public void testCABMenuShow() {
+        mDLAppHelper.populateContentInDLApp(10);
+        mDLAppHelper.launchApp(DownloadAppTestHelper.PACKAGE_NAME, DownloadAppTestHelper.APP_NAME);
+        mDevice.wait(Until.findObject(By.res("com.android.documentsui:id/dir_list")),
+                mDLAppHelper.TIMEOUT).getChildren().get(1).click(1 * 2 * mDLAppHelper.TIMEOUT);
+        UiObject2 cabMenuObj = null;
+        int counter = 5;
+        while ((cabMenuObj = mDevice.wait(Until.findObject(By.res(String.format("%s:id/menu_share",
+                DownloadAppTestHelper.PACKAGE_NAME))), mDLAppHelper.TIMEOUT)) == null
+                && counter-- > 0);
+        Assert.assertNotNull(cabMenuObj);
+        counter = 5;
+        while ((cabMenuObj = mDevice.wait(Until.findObject(By.res(String.format("%s:id/menu_delete",
+                DownloadAppTestHelper.PACKAGE_NAME))), mDLAppHelper.TIMEOUT)) == null
+                && counter-- > 0);
+        Assert.assertNotNull(cabMenuObj);
+        counter = 5;
+        while ((cabMenuObj = mDevice.wait(Until.findObject(
+                By.desc("More options")), mDLAppHelper.TIMEOUT)) == null && counter-- > 0)
+            ;
+        Assert.assertNotNull(cabMenuObj);
+        while ((cabMenuObj = mDevice.wait(Until.findObject(
+                By.desc("Done")), mDLAppHelper.TIMEOUT)) == null && counter-- > 0)
+            ;
+        Assert.assertNotNull(cabMenuObj);
+        cabMenuObj.click();
+        SystemClock.sleep(2 * mDLAppHelper.TIMEOUT);
+    }
+
+    @MediumTest
+    public void testCABMenuDelete() {
+        mDLAppHelper.populateContentInDLApp(10);
+        mDLAppHelper.launchApp(DownloadAppTestHelper.PACKAGE_NAME, DownloadAppTestHelper.APP_NAME);
+        UiObject2 deleteObj = mDevice.wait(Until.findObject(
+                By.res("com.android.documentsui:id/dir_list")), mDLAppHelper.TIMEOUT)
+                .getChildren().get(1);
+        String deleteObjText = deleteObj.getText();
+        deleteObj.click(1 * 2 * mDLAppHelper.TIMEOUT);
+        int counter = 5;
+        UiObject2 cabMenuObj = null;
+        while ((cabMenuObj = mDevice.wait(Until.findObject(By.res(String.format("%s:id/menu_delete",
+                DownloadAppTestHelper.PACKAGE_NAME))), 2 * mDLAppHelper.TIMEOUT)) == null
+                && counter-- > 0);
+        cabMenuObj.click();
+        UiObject2 deleteBtn = mDevice.wait(Until.findObject(
+                By.textContains("Delete")), mDLAppHelper.TIMEOUT);
+        if(deleteBtn != null) {
+            mDevice.wait(Until.findObject(By.text("OK")), 2 * mDLAppHelper.TIMEOUT).click();
+        }
+        Assert.assertFalse("", mDLAppHelper.getDownloadItemNames().contains(deleteObjText));
+    }
+}
diff --git a/tests/functional/externalstorage/Android.mk b/tests/functional/externalstorage/Android.mk
new file mode 100644
index 0000000..ade11f8
--- /dev/null
+++ b/tests/functional/externalstorage/Android.mk
@@ -0,0 +1,15 @@
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_SDK_VERSION := current
+
+LOCAL_SRC_FILES := $(call all-subdir-java-files)
+LOCAL_JAVA_LIBRARIES := android.test.runner
+LOCAL_STATIC_JAVA_LIBRARIES := launcher-helper-lib ub-uiautomator app-helpers
+
+LOCAL_PACKAGE_NAME := ExternalStorageFunctionalTests
+LOCAL_CERTIFICATE := platform
+
+include $(BUILD_PACKAGE)
\ No newline at end of file
diff --git a/tests/functional/externalstorage/AndroidManifest.xml b/tests/functional/externalstorage/AndroidManifest.xml
new file mode 100644
index 0000000..8230fc3
--- /dev/null
+++ b/tests/functional/externalstorage/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 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.functional.externalstoragetests">
+
+    <uses-sdk android:minSdkVersion="19"
+              android:targetSdkVersion="24" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <application>
+        <uses-library android:name="android.test.runner"/>
+    </application>
+    <instrumentation
+            android:name="android.test.InstrumentationTestRunner"
+            android:targetPackage="com.android.functional.externalstoragetests"
+            android:label="External Storage Functional Tests" />
+</manifest>
\ No newline at end of file
diff --git a/tests/functional/externalstorage/src/com/android/functional/externalstoragetests/AdoptableStorageTests.java b/tests/functional/externalstorage/src/com/android/functional/externalstoragetests/AdoptableStorageTests.java
new file mode 100644
index 0000000..84d18e2
--- /dev/null
+++ b/tests/functional/externalstorage/src/com/android/functional/externalstoragetests/AdoptableStorageTests.java
@@ -0,0 +1,215 @@
+/*
+ * 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.externalstoragetests;
+
+import android.app.UiAutomation;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager.NameNotFoundException;
+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 android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class AdoptableStorageTests extends InstrumentationTestCase {
+    private UiDevice mDevice = null;
+    private Context mContext = null;
+    private UiAutomation mUiAutomation = null;
+    private ExternalStorageHelper storageHelper;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mDevice = UiDevice.getInstance(getInstrumentation());
+        mContext = getInstrumentation().getContext();
+        mUiAutomation = getInstrumentation().getUiAutomation();
+        storageHelper = ExternalStorageHelper.getInstance(mDevice, mContext, mUiAutomation,
+                getInstrumentation());
+        mDevice.setOrientationNatural();
+    }
+
+    /**
+     * Tests external storage adoption and move data later flow via UI
+     */
+    @LargeTest
+    public void testAdoptAsAdoptableMoveDataLaterUIFlow() throws InterruptedException {
+        // ensure there is a storage to be adopted
+        storageHelper.partitionDisk("public");
+        initiateAdoption();
+        Pattern pattern = Pattern.compile("Move later", Pattern.CASE_INSENSITIVE);
+        mDevice.wait(Until.findObject(By.text(pattern)), storageHelper.TIMEOUT).click();
+        pattern = Pattern.compile("Next", Pattern.CASE_INSENSITIVE);
+        mDevice.wait(Until.findObject(By.text(pattern)), storageHelper.TIMEOUT).clickAndWait(
+                Until.newWindow(), storageHelper.TIMEOUT);
+        pattern = Pattern.compile("Done", Pattern.CASE_INSENSITIVE);
+        mDevice.wait(Until.findObject(By.text(pattern)), storageHelper.TIMEOUT).clickAndWait(
+                Until.newWindow(), storageHelper.TIMEOUT);
+        assertNotNull(storageHelper.getAdoptionVolumeId("private"));
+        // ensure data dirs have not moved
+        Intent intent = new Intent(Settings.ACTION_INTERNAL_STORAGE_SETTINGS);
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        mContext.startActivity(intent);
+        mDevice.wait(Until.findObject(By.textContains("SD card")), 2 * storageHelper.TIMEOUT)
+                .clickAndWait(Until.newWindow(), storageHelper.TIMEOUT);
+        assertTrue(mDevice.wait(Until.hasObject(By.res("android:id/title").text("Apps")),
+                storageHelper.TIMEOUT));
+    }
+
+    // Adoptable storage settings
+    /**
+     * tests to ensure that adoptable storage has setting options rename, eject, format as portable
+     */
+    @LargeTest
+    public void testAdoptableOverflowSettings() throws InterruptedException {
+        storageHelper.partitionDisk("private");
+        storageHelper.openSDCard();
+        Pattern pattern = Pattern.compile("More options", Pattern.CASE_INSENSITIVE);
+        UiObject2 moreOptions = mDevice.wait(Until.findObject(By.desc(pattern)),
+                storageHelper.TIMEOUT);
+        assertNotNull("Over flow menu options shouldn't be null", moreOptions);
+        moreOptions.click();
+        pattern = Pattern.compile("Rename", Pattern.CASE_INSENSITIVE);
+        assertTrue(mDevice.wait(Until.hasObject(By.text(pattern)), storageHelper.TIMEOUT));
+        pattern = Pattern.compile("Eject", Pattern.CASE_INSENSITIVE);
+        assertTrue(mDevice.wait(Until.hasObject(By.text(pattern)), storageHelper.TIMEOUT));
+        pattern = Pattern.compile("Format as portable", Pattern.CASE_INSENSITIVE);
+        assertTrue(mDevice.wait(Until.hasObject(By.text(pattern)), storageHelper.TIMEOUT));
+    }
+
+    /**
+     * tests to ensure that adoptable storage can be renamed
+     */
+    @LargeTest
+    public void testRenameAdoptable() throws InterruptedException {
+        storageHelper.partitionDisk("private");
+        storageHelper.openSDCard();
+        Pattern pattern = Pattern.compile("More options", Pattern.CASE_INSENSITIVE);
+        mDevice.wait(Until.findObject(By.desc(pattern)), storageHelper.TIMEOUT).click();
+        pattern = Pattern.compile("Rename", Pattern.CASE_INSENSITIVE);
+        mDevice.wait(Until.findObject(By.text(pattern)), storageHelper.TIMEOUT).click();
+        mDevice.wait(Until.findObject(By.res(storageHelper.SETTINGS_PKG, "edittext")),
+                storageHelper.TIMEOUT).setText("My SD card");
+        pattern = Pattern.compile("Save", Pattern.CASE_INSENSITIVE);
+        mDevice.wait(Until.findObject(By.text(pattern)), storageHelper.TIMEOUT).clickAndWait(
+                Until.newWindow(), storageHelper.TIMEOUT);
+        assertTrue(mDevice.wait(Until.hasObject(By.text("My SD card")), storageHelper.TIMEOUT));
+    }
+
+    /**
+     * tests to ensure that adoptable storage can be ejected
+     */
+    @LargeTest
+    public void testEjectAdoptable() throws InterruptedException {
+        storageHelper.partitionDisk("private");
+        storageHelper.openSDCard();
+        Pattern pattern = Pattern.compile("More options", Pattern.CASE_INSENSITIVE);
+        mDevice.wait(Until.findObject(By.desc(pattern)), storageHelper.TIMEOUT).click();
+        pattern = Pattern.compile("Eject", Pattern.CASE_INSENSITIVE);
+        mDevice.wait(Until.findObject(By.text(pattern)), storageHelper.TIMEOUT).click();
+        assertTrue(mDevice.wait(Until.hasObject(By.res(storageHelper.SETTINGS_PKG, "body")),
+                storageHelper.TIMEOUT));
+        mDevice.wait(Until.findObject(By.res(storageHelper.SETTINGS_PKG, "confirm").text(pattern)),
+                storageHelper.TIMEOUT).clickAndWait(Until.newWindow(), storageHelper.TIMEOUT);
+        pattern = Pattern.compile("Ejected", Pattern.CASE_INSENSITIVE);
+        assertTrue(mDevice.wait(Until.hasObject(By.res("android:id/summary").text(pattern)),
+                storageHelper.TIMEOUT));
+        mDevice.wait(Until.findObject(By.textContains("SD card")), storageHelper.TIMEOUT).click();
+        pattern = Pattern.compile("Mount", Pattern.CASE_INSENSITIVE);
+        mDevice.wait(Until.findObject(By.res("android:id/button1").text(pattern)),
+                2 * storageHelper.TIMEOUT).clickAndWait(Until.newWindow(), storageHelper.TIMEOUT);
+    }
+
+    /**
+     * tests to ensure that adoptable storage can be formated back as portable from settings
+     */
+    @LargeTest
+    public void testFormatAdoptableAsPortable() throws InterruptedException {
+        storageHelper.partitionDisk("private");
+        storageHelper.openSDCard();
+        Pattern pattern = Pattern.compile("More options", Pattern.CASE_INSENSITIVE);
+        mDevice.wait(Until.findObject(By.desc(pattern)), storageHelper.TIMEOUT).click();
+        pattern = Pattern.compile("Format as portable", Pattern.CASE_INSENSITIVE);
+        mDevice.wait(Until.findObject(By.text(pattern)), storageHelper.TIMEOUT)
+                .clickAndWait(Until.newWindow(), storageHelper.TIMEOUT);
+        mDevice.wait(Until.hasObject(
+                By.textContains("After formatting, you can use this")), storageHelper.TIMEOUT);
+        mDevice.wait(Until.findObject(By.text("FORMAT")), 2 * storageHelper.TIMEOUT)
+                .clickAndWait(Until.newWindow(), storageHelper.TIMEOUT);
+        pattern = Pattern.compile("Done", Pattern.CASE_INSENSITIVE);
+        mDevice.wait(Until.findObject(By.text(pattern)), 5 * storageHelper.TIMEOUT)
+                .clickAndWait(Until.newWindow(), storageHelper.TIMEOUT);
+    }
+
+    public void initiateAdoption() throws InterruptedException {
+        storageHelper.openSdCardSetUpNotification().clickAndWait(Until.newWindow(),
+                storageHelper.TIMEOUT);
+        UiObject2 adoptFlowUi = mDevice.wait(Until.findObject(
+                By.res(storageHelper.SETTINGS_PKG, "storage_wizard_init_internal_title")),
+                storageHelper.TIMEOUT);
+        adoptFlowUi.click();
+        Pattern pattern = Pattern.compile("NEXT", Pattern.CASE_INSENSITIVE);
+        adoptFlowUi = mDevice.wait(Until.findObject(
+                By.res(storageHelper.SETTINGS_PKG, "suw_navbar_next").text(pattern)),
+                storageHelper.TIMEOUT);
+        adoptFlowUi.clickAndWait(Until.newWindow(), storageHelper.TIMEOUT);
+        pattern = Pattern.compile("ERASE & FORMAT", Pattern.CASE_INSENSITIVE);
+        adoptFlowUi = mDevice.wait(Until.findObject(By.text(pattern)),
+                storageHelper.TIMEOUT);
+        adoptFlowUi.clickAndWait(Until.newWindow(), storageHelper.TIMEOUT);
+        adoptFlowUi = mDevice.wait(
+                Until.findObject(By.res(storageHelper.SETTINGS_PKG, "storage_wizard_progress")),
+                storageHelper.TIMEOUT);
+        assertNotNull(adoptFlowUi);
+        if ((mDevice.wait(Until.findObject(By.res("android:id/message")),
+                60 * storageHelper.TIMEOUT)) != null) {
+            mDevice.wait(Until.findObject(By.text("OK")), storageHelper.TIMEOUT).clickAndWait(
+                    Until.newWindow(), storageHelper.TIMEOUT);
+        }
+    }
+
+    /**
+     * System apps can't be moved to adopted storage
+     */
+    @LargeTest
+    public void testTransferSystemApp() throws InterruptedException, NameNotFoundException {
+        storageHelper.partitionDisk("private");
+        storageHelper.executeShellCommand("pm move-package " + storageHelper.SETTINGS_PKG + " "
+                + storageHelper.getAdoptionVolumeId("private"));
+        assertTrue(storageHelper.getInstalledLocation(storageHelper.SETTINGS_PKG)
+                .startsWith("/data/user_de/0"));
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        // Convert sdcard to public
+        storageHelper.executeShellCommand(String.format("sm partition %s %s",
+                storageHelper.getAdoptionDisk(), "public"));
+        Thread.sleep(storageHelper.TIMEOUT);
+        storageHelper.executeShellCommand("sm forget all");
+        Thread.sleep(storageHelper.TIMEOUT);
+        // move back to homescreen
+        mDevice.unfreezeRotation();
+        mDevice.pressBack();
+        mDevice.pressHome();
+        super.tearDown();
+    }
+}
diff --git a/tests/functional/externalstorage/src/com/android/functional/externalstoragetests/ExternalStorageHelper.java b/tests/functional/externalstorage/src/com/android/functional/externalstoragetests/ExternalStorageHelper.java
new file mode 100644
index 0000000..9a67ea4
--- /dev/null
+++ b/tests/functional/externalstorage/src/com/android/functional/externalstoragetests/ExternalStorageHelper.java
@@ -0,0 +1,288 @@
+/*
+ * 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.externalstoragetests;
+
+import android.app.Instrumentation;
+import android.app.UiAutomation;
+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.ParcelFileDescriptor;
+import android.os.StatFs;
+import android.os.SystemClock;
+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 android.util.Log;
+
+import android.platform.test.helpers.PlayStoreHelperImpl;
+
+import junit.framework.Assert;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class ExternalStorageHelper {
+    public static final String TEST_TAG = "StorageFunctionalTest";
+    public final String SETTINGS_PKG = "com.android.settings";
+    public final String PLAYSTORE_PKG = "com.android.vending";
+    public final String DOCUMENTS_PKG = "com.android.documentsui";
+    public static final Map<String, String> APPLIST = new HashMap<String, String>();
+    static {
+        APPLIST.put("w35location1", "com.test.w35location1");
+        APPLIST.put("w35location2", "com.test.w35location2");
+        APPLIST.put("w35location3", "com.test.w35location3");
+    }
+    public final int TIMEOUT = 2000;
+    public static ExternalStorageHelper mInstance = null;
+    public UiDevice mDevice;
+    public Context mContext;
+    public static UiAutomation mUiAutomation;
+    public static Instrumentation mInstrumentation;
+    public static Hashtable<String, List<String>> mPermissionGroupInfo = null;
+
+    public ExternalStorageHelper(UiDevice device, Context context, UiAutomation uiAutomation,
+            Instrumentation instrumentation) {
+        mDevice = device;
+        mContext = context;
+        mUiAutomation = uiAutomation;
+        mInstrumentation = instrumentation;
+    }
+
+    public static ExternalStorageHelper getInstance(UiDevice device, Context context,
+            UiAutomation uiAutomation, Instrumentation instrumentation) {
+        if (mInstance == null) {
+            mInstance = new ExternalStorageHelper(device, context, uiAutomation, instrumentation);
+        }
+        return mInstance;
+    }
+
+    /**
+     * Opens SD card setup notification from homescreen
+     */
+    public UiObject2 openSdCardSetUpNotification() throws InterruptedException {
+        boolean success = mDevice.openNotification();
+        Thread.sleep(TIMEOUT);
+        UiObject2 sdCardDetected = mDevice
+                .wait(Until.findObject(By.textContains("SD card detected")), TIMEOUT);
+        Assert.assertNotNull(sdCardDetected);
+        return sdCardDetected;
+    }
+
+    /**
+     * Open Storage settings, then SD Card
+     */
+    public void openStorageSettings() throws InterruptedException {
+        Intent intent = new Intent(Settings.ACTION_INTERNAL_STORAGE_SETTINGS);
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        mContext.startActivity(intent);
+        Thread.sleep(TIMEOUT * 2);
+    }
+
+    /**
+     * Open Storage settings, then SD Card
+     */
+    public void openSDCard() throws InterruptedException {
+        openStorageSettings();
+        mDevice.wait(Until.findObject(By.textContains("SD card")), TIMEOUT)
+                .clickAndWait(Until.newWindow(), TIMEOUT);
+    }
+
+    public String executeShellCommand(String command) {
+        ParcelFileDescriptor pfd = mUiAutomation.executeShellCommand(command);
+        try (BufferedReader reader = new BufferedReader(
+                new InputStreamReader(new FileInputStream(pfd.getFileDescriptor())))) {
+            String str = reader.readLine();
+            Log.d(TEST_TAG, String.format("Executing command: %s", command));
+            return str;
+        } catch (IOException e) {
+            Log.e(TEST_TAG, e.getMessage());
+        }
+
+        return null;
+    }
+
+    /**
+     * Create # of files in a given dir
+     */
+    public void createFiles(int numberOfFiles, String dir) {
+        for (int i = 0; i < numberOfFiles; ++i) {
+            if (!new File(String.format("%s/Test_%d", dir, i)).exists()) {
+                fillInStorage(dir, String.format("Test_%d", i), 1);
+            }
+        }
+    }
+
+    public void fillInStorage(String location, String filename, int sizeInKb) {
+        executeShellCommand(String.format("dd if=/dev/zero of=%s/%s bs=1024 count=%d",
+                location, filename, sizeInKb));
+    }
+
+    public int getFreeSpaceSize(File path) {
+        StatFs stat = new StatFs(path.getPath());
+        long blockSize = stat.getBlockSize();
+        long availableBlocks = stat.getAvailableBlocks();
+        return (int) ((availableBlocks * blockSize) / (1024 * 1024));
+    }
+
+    public boolean hasAdoptable() {
+        return Boolean.parseBoolean(executeShellCommand("sm has-adoptable").trim());
+    }
+
+    public String getAdoptionDisk() throws InterruptedException {
+        int counter = 10;
+        String disks = null;
+        while (((disks == null || disks.length() == 0)) && counter > 0) {
+            disks = executeShellCommand("sm list-disks adoptable");
+            Thread.sleep(TIMEOUT);
+            --counter;
+        }
+        if (counter == 0) {
+            throw new AssertionError("Devices must have adoptable media inserted");
+        }
+        return disks.split("\n")[0].trim();
+    }
+
+    public Boolean hasPublicVolume() {
+        return (null != executeShellCommand("sm list-volumes public"));
+    }
+
+    public String getAdoptionVolumeId(String volType) throws InterruptedException {
+        return getAdoptionVolumeInfo(volType).volId;
+    }
+
+    public String getAdoptionVolumeUuid(String volType) throws InterruptedException {
+        return getAdoptionVolumeInfo(volType).uuid;
+    }
+
+    public LocalVolumeInfo getAdoptionVolumeInfo(String volType) throws InterruptedException {
+        String[] lines = null;
+        int attempt = 0;
+        while (attempt++ < 5) {
+            if (null != (lines = executeShellCommand("sm list-volumes " + volType).split("\n"))) {
+                for (String line : lines) {
+                    final LocalVolumeInfo info = new LocalVolumeInfo(line.trim());
+                    if (info.volId.startsWith(volType) && "mounted".equals(info.state)) {
+                        return info;
+                    }
+                }
+                Thread.sleep(TIMEOUT);
+            }
+        }
+        return null;
+    }
+
+    public void partitionDisk(String type) throws InterruptedException {
+        if (type.equals("private")) {
+            executeShellCommand(String.format("sm partition %s %s", getAdoptionDisk(), type));
+            Thread.sleep(2 * TIMEOUT);
+        } else if (!hasPublicVolume() && type.equals("public")) {
+            executeShellCommand("sm forget all");
+            executeShellCommand(String.format("sm partition %s %s", getAdoptionDisk(), type));
+            Thread.sleep(2 * TIMEOUT);
+            setupAsPortableUiFlow();
+        }
+    }
+
+    public void setupAsPortableUiFlow() throws InterruptedException {
+        openSdCardSetUpNotification();
+        Thread.sleep(TIMEOUT);
+        Pattern pattern = Pattern.compile("Set up", Pattern.CASE_INSENSITIVE);
+        UiObject2 adoptFlowUi = mDevice.wait(Until.findObject(By.desc(pattern)), TIMEOUT);
+        adoptFlowUi.clickAndWait(Until.newWindow(), TIMEOUT);
+        adoptFlowUi = mDevice.wait(Until.findObject(
+                By.res(SETTINGS_PKG, "storage_wizard_init_external_title")),
+                TIMEOUT);
+        adoptFlowUi.click();
+        pattern = Pattern.compile("Next", Pattern.CASE_INSENSITIVE);
+        adoptFlowUi = mDevice.wait(Until.findObject(By.text(pattern)),
+                TIMEOUT);
+        adoptFlowUi.clickAndWait(Until.newWindow(), TIMEOUT);
+        pattern = Pattern.compile("Done", Pattern.CASE_INSENSITIVE);
+        mDevice.wait(Until.findObject(By.text(pattern)), TIMEOUT).clickAndWait(
+                Until.newWindow(), TIMEOUT);
+        hasPublicVolume();
+
+    }
+
+    public void installFromPlayStore(String appName) {
+        PlayStoreHelperImpl mHelper = new PlayStoreHelperImpl(mInstrumentation);
+        mHelper.open();
+        mHelper.doSearch(appName);
+        mHelper.selectFirstResult();
+        mDevice.wait(Until.findObject(By.res(PLAYSTORE_PKG, "buy_button").text("INSTALL")),
+                TIMEOUT).clickAndWait(Until.newWindow(), 2 * TIMEOUT);
+        SystemClock.sleep(2 * TIMEOUT);
+        mDevice.wait(Until.findObject(By.res(PLAYSTORE_PKG, "launch_button").text("OPEN")),
+                5 * TIMEOUT);
+    }
+
+    public PackageInfo getPackageInfo(String packageName) throws NameNotFoundException {
+        return mContext.getPackageManager().getPackageInfo(packageName, 0);
+    }
+
+    public Boolean doesPackageExist(String packageName) throws NameNotFoundException {
+        try {
+            mContext.getPackageManager().getPackageInfo(packageName, 0);
+        } catch (NameNotFoundException nex) {
+            throw nex;
+        }
+
+        return Boolean.TRUE;
+    }
+
+    public String getInstalledLocation(String packageName) throws NameNotFoundException {
+        Assert.assertTrue(String.format("%s doesn't exist!", packageName),
+                doesPackageExist(packageName));
+        return getPackageInfo(packageName).applicationInfo.dataDir;
+    }
+
+    public void settingsUiCleanUp() {
+        executeShellCommand("pm clear " + SETTINGS_PKG);
+        executeShellCommand("pm clear " + DOCUMENTS_PKG);
+    }
+
+    private static class LocalVolumeInfo {
+        public String volId;
+        public String state;
+        public String uuid;
+
+        public LocalVolumeInfo(String line) {
+            final String[] split = line.split(" ");
+            volId = split[0];
+            state = split[1];
+            uuid = split[2];
+        }
+    }
+
+    public PackageManager getPackageManager() {
+        return mContext.getPackageManager();
+    }
+}
diff --git a/tests/functional/externalstorage/src/com/android/functional/externalstoragetests/PortableStorageTests.java b/tests/functional/externalstorage/src/com/android/functional/externalstoragetests/PortableStorageTests.java
new file mode 100644
index 0000000..79cdfe2
--- /dev/null
+++ b/tests/functional/externalstorage/src/com/android/functional/externalstoragetests/PortableStorageTests.java
@@ -0,0 +1,258 @@
+/*
+ * 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.externalstoragetests;
+
+import android.app.UiAutomation;
+import android.content.Context;
+import android.content.Intent;
+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 android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import junit.framework.Assert;
+
+public class PortableStorageTests extends InstrumentationTestCase {
+    private UiDevice mDevice = null;
+    private Context mContext = null;
+    private UiAutomation mUiAutomation = null;
+    private ExternalStorageHelper storageHelper;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mDevice = UiDevice.getInstance(getInstrumentation());
+        mContext = getInstrumentation().getContext();
+        mUiAutomation = getInstrumentation().getUiAutomation();
+        storageHelper = ExternalStorageHelper.getInstance(mDevice, mContext, mUiAutomation,
+                getInstrumentation());
+        mDevice.setOrientationNatural();
+    }
+
+    /**
+     * Test to ensure sd card can be adopted as portable storage
+     */
+    @LargeTest
+    public void testAdoptAsPortableViaUI() throws InterruptedException {
+        // ensure notification
+        storageHelper.executeShellCommand(String.format(
+                "sm partition %s %s", storageHelper.getAdoptionDisk(), "public"));
+        Thread.sleep(storageHelper.TIMEOUT);
+        storageHelper.setupAsPortableUiFlow();
+        storageHelper.executeShellCommand(String.format("sm forget all"));
+        Thread.sleep(storageHelper.TIMEOUT);
+    }
+
+    /**
+     * tests to ensure copy option is visible for items on portable storage
+     */
+    @LargeTest
+    public void testCopyFromPortable() throws InterruptedException {
+        ensureHasPortable();
+        storageHelper.createFiles(2,
+                String.format("/storage/%s", storageHelper.getAdoptionVolumeUuid("public")));
+        storageHelper.openSDCard();
+        mDevice.wait(Until.findObject(By.res("android:id/title").text("Test_0")),
+                storageHelper.TIMEOUT).click(storageHelper.TIMEOUT);
+        mDevice.wait(Until.findObject(By.desc(Pattern.compile("More options",
+                Pattern.CASE_INSENSITIVE))), storageHelper.TIMEOUT).click();
+        assertNotNull(mDevice.wait(Until.findObject(By.res("android:id/title").text("Copy to…")),
+                2 * storageHelper.TIMEOUT));
+        mDevice.wait(Until.findObject(By.res("android:id/title").text("Copy to…")),
+                storageHelper.TIMEOUT).clickAndWait(Until.newWindow(), storageHelper.TIMEOUT);
+        mDevice.pressBack();
+    }
+
+    /**
+     * tests to ensure that resources on portable storage can be deleted via UI
+     */
+    @LargeTest
+    public void testDeleteFromPortable() throws InterruptedException {
+        ensureHasPortable();
+        storageHelper.createFiles(2,
+                String.format("/storage/%s", storageHelper.getAdoptionVolumeUuid("public")));
+        storageHelper.openSDCard();
+        mDevice.wait(Until.findObject(By.res("android:id/title").text("Test_0")),
+                storageHelper.TIMEOUT).click(storageHelper.TIMEOUT);
+        mDevice.wait(Until.findObject(By.res("com.android.documentsui:id/menu_sort")),
+                storageHelper.TIMEOUT).clickAndWait(Until.newWindow(), storageHelper.TIMEOUT);
+        assertNull(mDevice.wait(Until.findObject(By.res("android:id/title").text("Test_0")),
+                2 * storageHelper.TIMEOUT));
+    }
+
+    /**
+     * tests to ensure that external storage is explorable via UI
+     */
+    @LargeTest
+    public void testExplorePortable() throws InterruptedException {
+        ensureHasPortable();
+        // Create 2 random files on SDCard
+        storageHelper.createFiles(2,
+                String.format("/storage/%s", storageHelper.getAdoptionVolumeUuid("public")));
+        Intent intent = new Intent(Settings.ACTION_INTERNAL_STORAGE_SETTINGS);
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        mContext.startActivity(intent);
+        Thread.sleep(storageHelper.TIMEOUT * 2);
+        mDevice.wait(Until.findObject(By.textContains("SD card")), storageHelper.TIMEOUT)
+                .clickAndWait(Until.newWindow(), storageHelper.TIMEOUT);
+        for (int i = 0; i < 2; ++i) {
+            Assert.assertTrue(mDevice.wait(Until.hasObject(By.res("android:id/title")
+                    .text(String.format("Test_%d", i))), storageHelper.TIMEOUT));
+        }
+    }
+
+    /**
+     * tests to ensure that resources on portable storage can be shared via UI
+     */
+    @LargeTest
+    public void testShareableFromPortable() throws InterruptedException {
+        ensureHasPortable();
+        storageHelper.createFiles(2,
+                String.format("/storage/%s", storageHelper.getAdoptionVolumeUuid("public")));
+        storageHelper.openSDCard();
+        mDevice.wait(Until.findObject(By.res("android:id/title").text("Test_0")),
+                storageHelper.TIMEOUT).click(storageHelper.TIMEOUT);
+        mDevice.wait(Until.findObject(By.res("com.android.documentsui:id/menu_list")),
+                storageHelper.TIMEOUT).click();
+        assertNotNull(mDevice.wait(Until.findObject(By.res("android:id/resolver_list")),
+                storageHelper.TIMEOUT));
+        // click and ensure intent is sent to share? or actual share?
+        mDevice.pressBack();
+    }
+
+    /**
+     * tests to ensure that portable overflow menu contain all setting options
+     */
+    @LargeTest
+    public void testPortableOverflowSettings() throws InterruptedException {
+        ensureHasPortable();
+        storageHelper.createFiles(2,
+                String.format("/storage/%s", storageHelper.getAdoptionVolumeUuid("public")));
+        storageHelper.openSDCard();
+        mDevice.wait(Until.findObject(By.res("android:id/title").text("Test_0")),
+                storageHelper.TIMEOUT).click(storageHelper.TIMEOUT);
+        assertTrue(mDevice.wait(Until.hasObject(By.res(storageHelper.DOCUMENTS_PKG, "menu_search")),
+                storageHelper.TIMEOUT));
+        assertTrue(mDevice.wait(Until.hasObject(By.res(storageHelper.DOCUMENTS_PKG, "menu_sort")),
+                storageHelper.TIMEOUT));
+        assertTrue(mDevice.wait(Until.hasObject(By.text("1 selected")), storageHelper.TIMEOUT));
+    }
+
+    /**
+     * tests to ensure that portable storage has setting options format, format as internal, eject
+     */
+    @LargeTest
+    public void testPortableSettings() throws InterruptedException {
+        ensureHasPortable();
+        storageHelper.openSDCard();
+        Pattern pattern = Pattern.compile("More options", Pattern.CASE_INSENSITIVE);
+        mDevice.wait(Until.findObject(By.desc(pattern)), storageHelper.TIMEOUT).click();
+        pattern = Pattern.compile("Storage settings", Pattern.CASE_INSENSITIVE);
+        mDevice.wait(Until.findObject(By.text(pattern)), storageHelper.TIMEOUT)
+                .clickAndWait(
+                        Until.newWindow(), storageHelper.TIMEOUT);
+        pattern = Pattern.compile("Eject", Pattern.CASE_INSENSITIVE);
+        assertTrue(mDevice.wait(Until.hasObject(By.text(pattern)), storageHelper.TIMEOUT));
+        pattern = Pattern.compile("Format", Pattern.CASE_INSENSITIVE);
+        assertTrue(mDevice.wait(Until.hasObject(By.text(pattern)), storageHelper.TIMEOUT));
+        pattern = Pattern.compile("Format as internal", Pattern.CASE_INSENSITIVE);
+        assertTrue(mDevice.wait(Until.hasObject(By.text(pattern)),
+                storageHelper.TIMEOUT));
+    }
+
+    /**
+     * tests to ensure that portable storage can be ejected from settings
+     */
+    @LargeTest
+    public void testEjectPortable() throws InterruptedException {
+        ensureHasPortable();
+        storageHelper.openStorageSettings();
+        mDevice.wait(Until.findObject(By.res(storageHelper.SETTINGS_PKG, "unmount")),
+                storageHelper.TIMEOUT).click();
+        assertTrue(mDevice.wait(Until.hasObject(By.res("android:id/summary").text("Ejected")),
+                4 * storageHelper.TIMEOUT));
+        mDevice.wait(Until.findObject(By.textContains("SD card")), 2 * storageHelper.TIMEOUT)
+                .click();
+        Pattern pattern = Pattern.compile("Mount", Pattern.CASE_INSENSITIVE);
+        mDevice.wait(Until.findObject(By.res("android:id/button1").text(pattern)),
+                2 * storageHelper.TIMEOUT).clickAndWait(Until.newWindow(), storageHelper.TIMEOUT);
+        ;
+    }
+
+    /**
+     * tests to ensure that portable storage can be erased and formated from settings
+     */
+    @LargeTest
+    public void testFormatPortable() throws InterruptedException {
+        ensureHasPortable();
+        storageHelper.openSDCard();
+        Pattern pattern = Pattern.compile("More options", Pattern.CASE_INSENSITIVE);
+        mDevice.wait(Until.findObject(By.desc(pattern)), storageHelper.TIMEOUT).click();
+        pattern = Pattern.compile("Storage settings", Pattern.CASE_INSENSITIVE);
+        mDevice.wait(Until.findObject(By.text(pattern)), storageHelper.TIMEOUT)
+                .clickAndWait(
+                        Until.newWindow(), storageHelper.TIMEOUT);
+        UiObject2 format = mDevice.wait(Until.findObject(By.text("Format")), storageHelper.TIMEOUT);
+        format.clickAndWait(Until.newWindow(), storageHelper.TIMEOUT);
+        pattern = Pattern.compile("Erase & Format", Pattern.CASE_INSENSITIVE);
+        mDevice.wait(Until.findObject(By.text(pattern)), storageHelper.TIMEOUT).click();
+        pattern = Pattern.compile("Done", Pattern.CASE_INSENSITIVE);
+        mDevice.wait(Until.findObject(By.text(pattern)), 20 * storageHelper.TIMEOUT).click();
+    }
+
+    /**
+     * tests to ensure that portable storage can be erased and formated as internal from settings
+     */
+    @LargeTest
+    public void testFormatPortableAsAdoptable() throws InterruptedException {
+        try {
+            ensureHasPortable();
+            storageHelper.openSDCard();
+            Pattern pattern = Pattern.compile("More options", Pattern.CASE_INSENSITIVE);
+            mDevice.wait(Until.findObject(By.desc(pattern)), storageHelper.TIMEOUT).click();
+            pattern = Pattern.compile("Storage settings", Pattern.CASE_INSENSITIVE);
+            mDevice.wait(Until.findObject(By.text(pattern)), storageHelper.TIMEOUT)
+                    .clickAndWait(
+                            Until.newWindow(), storageHelper.TIMEOUT);
+            pattern = Pattern.compile("Format", Pattern.CASE_INSENSITIVE);
+            assertTrue(mDevice.wait(Until.hasObject(By.text(pattern)), storageHelper.TIMEOUT));
+            pattern = Pattern.compile("Format as internal", Pattern.CASE_INSENSITIVE);
+            assertTrue(mDevice.wait(Until.hasObject(By.text(pattern)),
+                    storageHelper.TIMEOUT));
+            // Next flow is same as adoption, so no need to test
+        } finally {
+            storageHelper.partitionDisk("public");
+        }
+    }
+
+    private void ensureHasPortable() throws InterruptedException {
+        storageHelper.partitionDisk("public");
+        storageHelper.settingsUiCleanUp();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        mDevice.unfreezeRotation();
+        mDevice.pressBack();
+        mDevice.pressHome();
+        super.tearDown();
+    }
+}
diff --git a/tests/functional/launchertests/Android.mk b/tests/functional/launchertests/Android.mk
new file mode 100644
index 0000000..5166049
--- /dev/null
+++ b/tests/functional/launchertests/Android.mk
@@ -0,0 +1,26 @@
+# 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.
+
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_PACKAGE_NAME := LauncherFunctionalTests
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_STATIC_JAVA_LIBRARIES := ub-uiautomator timeresult-helper-lib launcher-helper-lib android-support-test
+
+LOCAL_SDK_VERSION := current
+
+include $(BUILD_PACKAGE)
diff --git a/tests/functional/launchertests/AndroidManifest.xml b/tests/functional/launchertests/AndroidManifest.xml
new file mode 100644
index 0000000..86b75f3
--- /dev/null
+++ b/tests/functional/launchertests/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.launcher.functional">
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <uses-sdk android:minSdkVersion="19"
+          android:targetSdkVersion="23"/>
+
+    <instrumentation
+            android:name="android.support.test.runner.AndroidJUnitRunner"
+            android:targetPackage="android.launcher.functional"
+            android:label="Platform Android Launcher Functional Tests" />
+</manifest>
diff --git a/tests/functional/launchertests/src/com/android/launcher/functional/HomeScreenTests.java b/tests/functional/launchertests/src/com/android/launcher/functional/HomeScreenTests.java
new file mode 100644
index 0000000..9f31232
--- /dev/null
+++ b/tests/functional/launchertests/src/com/android/launcher/functional/HomeScreenTests.java
@@ -0,0 +1,305 @@
+/*
+ * 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.launcher.functional;
+
+import java.io.File;
+import java.io.IOException;
+
+import android.app.UiAutomation;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.Context;
+import android.graphics.Point;
+import android.os.PowerManager;
+import android.os.RemoteException;
+import android.os.SystemClock;
+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.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.util.Log;
+import android.view.KeyEvent;
+
+public class HomeScreenTests extends InstrumentationTestCase {
+
+    private static final int TIMEOUT = 3000;
+    private static final String HOTSEAT = "hotseat";
+    private UiDevice mDevice;
+    private PackageManager mPackageManager;
+    private ILauncherStrategy mLauncherStrategy = null;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mDevice = UiDevice.getInstance(getInstrumentation());
+        mPackageManager = getInstrumentation().getContext().getPackageManager();
+        mDevice.setOrientationNatural();
+        mLauncherStrategy = LauncherStrategyFactory.getInstance(mDevice).getLauncherStrategy();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        mDevice.pressHome();
+        mDevice.unfreezeRotation();
+        mDevice.waitForIdle();
+        super.tearDown();
+    }
+
+    public String getLauncherPackage() {
+        return mDevice.getLauncherPackageName();
+    }
+
+    public void launchAppWithIntent(String appPackageName) {
+        Intent appIntent = mPackageManager.getLaunchIntentForPackage(appPackageName);
+        appIntent.addCategory(Intent.CATEGORY_LAUNCHER);
+        appIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        getInstrumentation().getContext().startActivity(appIntent);
+        SystemClock.sleep(TIMEOUT);
+    }
+
+    @MediumTest
+    public void testGoHome() {
+        launchAppWithIntent("com.android.chrome");
+        mDevice.pressHome();
+        UiObject2 hotseat = mDevice.findObject(By.res(getLauncherPackage(), HOTSEAT));
+        assertNotNull("Hotseat could not be found", hotseat);
+    }
+
+    @MediumTest
+    public void testHomeToRecentsNavigation() throws Exception {
+        mDevice.pressRecentApps();
+        assertNotNull("Recents not found when navigating from hotseat",
+                mDevice.wait(Until.hasObject(By.res("com.android.systemui:id/recents_view")),
+                TIMEOUT));
+    }
+
+    @MediumTest
+    public void testCreateAndDeleteShortcutOnHome() throws Exception {
+        createShortcutOnHome("Calculator");
+        // Verify presence of shortcut on Home screen
+        UiObject2 hotseat = mDevice.findObject(By.res(getLauncherPackage(), HOTSEAT));
+        assertNotNull("Not on Home page; hotseat could not be found", hotseat);
+        UiObject2 calculatorIcon = mDevice.wait(Until.findObject(By.text("Calculator")), TIMEOUT);
+        assertNotNull("Calculator shortcut not found on Home screen", calculatorIcon);
+        removeObjectFromHomeScreen(calculatorIcon, "text", "Calculator");
+    }
+
+    // Screen on with power button press
+    @MediumTest
+    public void testScreenOffOnUsingPowerButton() throws Exception {
+        Context currentContext = getInstrumentation().getContext();
+        PowerManager pm = (PowerManager) currentContext
+                .getSystemService(currentContext.POWER_SERVICE);
+        mDevice.pressKeyCode(KeyEvent.KEYCODE_POWER);
+        Thread.sleep(TIMEOUT);
+        assertFalse("Screen wasn't turned off", pm.isInteractive());
+        mDevice.pressKeyCode(KeyEvent.KEYCODE_POWER);
+        Thread.sleep(TIMEOUT);
+        assertTrue("Screen wasn't turned on by pressing Power Key", pm.isInteractive());
+        // Unlock screen since this ends up putting the device in Swpe lock mode.
+        mDevice.wakeUp();
+        mDevice.pressMenu();
+    }
+
+    // Wallpaper menu from home page
+    @MediumTest
+    public void testLongPressFromHomeToWallpaperMenu() {
+        String wallpaperResourceId = getLauncherPackage() + ":id/wallpaper_image";
+        verifyHomeLongPressMenu("Wallpaper", "wallpaper_button", wallpaperResourceId);
+    }
+
+    // Widget menu from home page
+    @MediumTest
+    public void testLongPressFromHomeToWidgetMenu() {
+        String widgetResourceId = getLauncherPackage() + ":id/widgets_list_view";
+        verifyHomeLongPressMenu("Widgets", "widget_button", widgetResourceId);
+    }
+
+    // Settings menu from home page
+    @MediumTest
+    public void testLongPressFromHomeToGoogleSettingsMenu() {
+         verifyHomeLongPressMenu("Google settings", "settings_button",
+                 "android:id/action_bar");
+     }
+
+    // Home screen long press display menu
+    private void verifyHomeLongPressMenu(String longPressElementName,
+            String longPressElementResourceId, String pageLoadResourceId) {
+        mDevice.pressHome();
+        UiObject2 workspace = mDevice.findObject(By.res(getLauncherPackage(), "workspace"));
+        workspace.longClick();
+        UiObject2 longPressElementButton = mDevice.findObject(By.res(getLauncherPackage(),
+                longPressElementResourceId));
+        assertNotNull(longPressElementName +
+                " element is not visible on long press", longPressElementButton);
+        longPressElementButton.click();
+        mDevice.waitForIdle();
+        assertNotNull(longPressElementName + " page hasn't loaded correctly on clicking",
+                mDevice.wait(Until.hasObject(By.res(pageLoadResourceId)), TIMEOUT));
+    }
+
+    // Home screen add widget
+    @MediumTest
+    public void testAddRemoveWidgetOnHome() throws Exception {
+        mDevice.pressHome();
+        mDevice.waitForIdle();
+        UiObject2 workspace = mDevice.findObject(By.res(getLauncherPackage(), "workspace"));
+        workspace.longClick();
+        UiObject2 widgetButton = mDevice.findObject(By.res(getLauncherPackage(),
+                "widget_button"));
+        widgetButton.click();
+        mDevice.waitForIdle();
+        UiObject2 analogClock = mDevice.wait(Until.findObject(By.text("Analog clock")), TIMEOUT);
+        analogClock.click(2000L);
+
+        // Verify presence of shortcut on Home screen
+        UiObject2 hotseat = mDevice.findObject(By.res(getLauncherPackage(), HOTSEAT));
+        assertNotNull("Not on Home page; hotseat could not be found", hotseat);
+        UiObject2 analogClockWidget = mDevice.findObject
+                (By.res("com.google.android.deskclock:id/analog_appwidget"));
+        assertNotNull("Clock widget not found on Home screen", analogClockWidget);
+        removeObjectFromHomeScreen(analogClockWidget, "res",
+                "com.google.android.deskclock:id/analog_appwidget");
+    }
+
+    @MediumTest
+    public void testCreateRenameRemoveFolderOnHome() throws Exception {
+        // Create two shortcuts on the home screen
+        createShortcutOnHome("Calculator");
+        createShortcutOnHome("Clock");
+        mDevice.pressHome();
+        mDevice.waitForIdle();
+
+        // Drag and drop the calculator shortcut onto
+        // the clock shortcut to create a folder.
+        UiObject2 calculatorIcon = mDevice.wait
+                (Until.findObject(By.text("Calculator")), TIMEOUT);
+        UiObject2 clockIcon = mDevice.wait
+                (Until.findObject(By.text("Clock")), TIMEOUT);
+        calculatorIcon.drag(clockIcon.getVisibleCenter(), 1000);
+
+        // Verify that there is a new unnamed folder at this point
+        UiObject2 customFolder = mDevice.wait
+                (Until.findObject(By.desc("Folder: ")), TIMEOUT);
+        customFolder.click();
+        UiObject2 unnamedFolder = mDevice.wait
+                (Until.findObject(By.text("Unnamed Folder")), TIMEOUT);
+        assertNotNull("Custom folder not created", unnamedFolder);
+
+        // Rename the unnamed folder to 'Snowflake'
+        unnamedFolder.click();
+        unnamedFolder.setText("Snowflake");
+
+        // Dismiss the IME and then collapse the folder.
+        mDevice.pressBack();
+        mDevice.pressBack();
+        mDevice.waitForIdle();
+        UiObject2 workspace = mDevice.findObject(By.res(getLauncherPackage(), "workspace"));
+        workspace.click();
+
+        // Verify the newly renamed Snowflake folder
+        UiObject2 snowflakeFolder = mDevice.wait
+                (Until.findObject(By.text("Snowflake")), TIMEOUT);
+        assertNotNull("Custom folder not created", snowflakeFolder);
+
+        // Verify that the Snowflake folder can be removed
+        removeObjectFromHomeScreen(snowflakeFolder, "text", "Snowflake");
+    }
+
+    // Folders - opening an app from folder
+    @MediumTest
+    public void testOpenAppFromFolderOnHome() {
+        mDevice.pressHome();
+        mDevice.waitForIdle();
+        UiObject2 googleFolder = mDevice.wait
+                (Until.findObject(By.desc("Folder: Google")), TIMEOUT);
+        googleFolder.click();
+        UiObject2 youTubeButton = mDevice.wait
+                (Until.findObject(By.text("YouTube")), TIMEOUT);
+        youTubeButton.click();
+        assertTrue("Youtube wasn't opened from the Google folder",
+                mDevice.wait(Until.hasObject
+                (By.pkg("com.google.android.youtube")), TIMEOUT));
+        mDevice.pressHome();
+        mDevice.waitForIdle();
+    }
+
+    /* This method takes in an object to be drag/dropped onto the
+     * Remove button hiding behind the search bar
+     *
+     * @param objectToRemove The UI object to be removed from the
+     * Home screen
+     * @param searchCategory String of value text, res or desc, based on which
+     * the By selector is chosen to find the object
+     * @param searchContent String to be searched in the searchCategory
+     */
+    private void removeObjectFromHomeScreen(UiObject2 objectToRemove,
+            String searchCategory, String searchContent) {
+        // Find the center of the Google Search Bar that the Remove button
+        // is hidden behind.
+        // Note: We're using this hacky way of locating the Remove button
+        // because today, UIAutomator doesn't allow us to search for an element
+        // while a touchdown has been executed, but before the touch up.
+        // FYI: A click is a combination of a touch down and a touch up motion.
+        UiObject2 removeButton = mDevice.wait(Until.findObject(By.desc("Google Search")),
+                TIMEOUT);
+        // Drag the calculator icon to the 'Remove' button to remove it
+        objectToRemove.drag(new Point(mDevice.getDisplayWidth() / 2,
+                 removeButton.getVisibleCenter().y), 1000);
+
+        UiObject2 checkForObject = null;
+        // Refetch the calculator icon
+        if (searchCategory.equals("text")) {
+            checkForObject = mDevice.findObject(By.text(searchContent));
+        }
+        else if (searchCategory.equals("res")) {
+            checkForObject = mDevice.findObject(By.res(searchContent));
+        }
+        else if (searchCategory.equals("desc")) {
+            checkForObject = mDevice.findObject(By.desc(searchContent));
+        }
+        else {
+            Log.d(null, "Your search category doesn't match common use cases.");
+        }
+        assertNull(searchContent + " is present on the Home screen after removal attempt",
+                checkForObject);
+    }
+
+    /* Creates a shortcut for the given app name on the Home screen
+     *
+     * @param appName text of the app name as seen in 'All Apps'
+     */
+    private void createShortcutOnHome(String appName) throws Exception {
+        // Navigate to All Apps
+        mDevice.pressHome();
+        UiObject2 allApps = mDevice.findObject(By.desc("Apps"));
+        allApps.click();
+        mDevice.waitForIdle();
+
+        // Long press on the Calculator app for two seconds and release on home screen
+        // to create a shortcut
+        UiObject2 appIcon =  mDevice.wait(Until.findObject
+                (By.res(getLauncherPackage(), "icon").text(appName)), TIMEOUT);
+        appIcon.click(2000L);
+    }
+}
diff --git a/tests/functional/launchertests/src/com/android/launcher/functional/HotseatHelper.java b/tests/functional/launchertests/src/com/android/launcher/functional/HotseatHelper.java
new file mode 100644
index 0000000..c0b2c51
--- /dev/null
+++ b/tests/functional/launchertests/src/com/android/launcher/functional/HotseatHelper.java
@@ -0,0 +1,64 @@
+/*
+ * 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.launcher.functional;
+
+import java.io.IOException;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Environment;
+import android.os.RemoteException;
+import android.os.SystemClock;
+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;
+
+
+public class HotseatHelper {
+
+    private static final int TIMEOUT = 3000;
+    private UiDevice mDevice;
+    private PackageManager pm;
+    private Context mContext;
+    public static HotseatHelper mInstance = null;
+
+    private HotseatHelper(UiDevice device, Context context) {
+        mDevice = device;
+        mContext = context;
+    }
+
+    public static HotseatHelper getInstance(UiDevice device, Context context) {
+        if (mInstance == null) {
+            mInstance = new HotseatHelper(device, context);
+        }
+        return mInstance;
+    }
+
+    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);
+    }
+}
diff --git a/tests/functional/launchertests/src/com/android/launcher/functional/PhoneHotseatTests.java b/tests/functional/launchertests/src/com/android/launcher/functional/PhoneHotseatTests.java
new file mode 100644
index 0000000..9a987f3
--- /dev/null
+++ b/tests/functional/launchertests/src/com/android/launcher/functional/PhoneHotseatTests.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.launcher.functional;
+
+import java.io.IOException;
+import android.content.Intent;
+import android.os.RemoteException;
+import android.os.SystemClock;
+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.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+
+public class PhoneHotseatTests extends InstrumentationTestCase {
+
+    private static final int TIMEOUT = 3000;
+    private static final String HOTSEAT = "hotseat";
+    private UiDevice mDevice;
+    private HotseatHelper hotseatHelper = null;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mDevice = UiDevice.getInstance(getInstrumentation());
+        try {
+            mDevice.setOrientationNatural();
+        } catch (RemoteException e) {
+            throw new RuntimeException("failed to freeze device orientaion", e);
+        }
+        hotseatHelper = HotseatHelper.getInstance(mDevice, getInstrumentation().getContext());
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        mDevice.pressHome();
+        super.tearDown();
+    }
+
+    public String getLauncherPackage() {
+        return mDevice.getLauncherPackageName();
+    }
+
+    @MediumTest
+    public void testOpenPhoneFromHotseat() {
+        hotseatHelper.launchAppFromHotseat("Phone", "com.google.android.dialer");
+    }
+
+    @MediumTest
+    public void testOpenMessengerFromHotseat() {
+        hotseatHelper.launchAppFromHotseat("Messenger", "com.google.android.apps.messaging");
+    }
+
+    @MediumTest
+    public void testOpenChromeFromHotseat() {
+        hotseatHelper.launchAppFromHotseat("Chrome", "com.android.chrome");
+    }
+
+    @MediumTest
+    public void testOpenCameraFromHotseat() {
+        hotseatHelper.launchAppFromHotseat("Camera", "com.google.android.GoogleCamera");
+    }
+
+    @MediumTest
+    public void testHomeToAllAppsNavigation() {
+        hotseatHelper.launchAppFromHotseat("Apps", getLauncherPackage());
+        assertNotNull("All apps page not found when navigating from hotseat",
+                mDevice.wait(Until.hasObject(By.res(getLauncherPackage(), "apps_view")), TIMEOUT));
+    }
+
+}
diff --git a/tests/functional/launchertests/src/com/android/launcher/functional/TabletHotseatTests.java b/tests/functional/launchertests/src/com/android/launcher/functional/TabletHotseatTests.java
new file mode 100644
index 0000000..7b16a86
--- /dev/null
+++ b/tests/functional/launchertests/src/com/android/launcher/functional/TabletHotseatTests.java
@@ -0,0 +1,104 @@
+/*
+ * 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.launcher.functional;
+
+import java.io.IOException;
+import android.content.Intent;
+import android.os.RemoteException;
+import android.os.SystemClock;
+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.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.test.suitebuilder.annotation.Suppress;
+
+public class TabletHotseatTests extends InstrumentationTestCase {
+
+    private static final int TIMEOUT = 3000;
+    private static final String HOTSEAT = "hotseat";
+    private UiDevice mDevice;
+    private HotseatHelper hotseatHelper = null;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mDevice = UiDevice.getInstance(getInstrumentation());
+        try {
+            mDevice.setOrientationNatural();
+        } catch (RemoteException e) {
+            throw new RuntimeException("failed to freeze device orientaion", e);
+        }
+        hotseatHelper = HotseatHelper.getInstance(mDevice, getInstrumentation().getContext());
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        mDevice.pressHome();
+        super.tearDown();
+    }
+
+    public String getLauncherPackage() {
+        return mDevice.getLauncherPackageName();
+    }
+
+    @Suppress
+    @MediumTest
+    public void testOpenChromeFromHotseat() {
+        hotseatHelper.launchAppFromHotseat("Chrome", "com.android.chrome");
+    }
+
+    @Suppress
+    @MediumTest
+    public void testOpenCameraFromHotseat() {
+        hotseatHelper.launchAppFromHotseat("Camera", "com.google.android.GoogleCamera");
+    }
+
+    @Suppress
+    @MediumTest
+    public void testOpenGMailFromHotseat() {
+        hotseatHelper.launchAppFromHotseat("Gmail", "com.google.android.gm");
+    }
+
+    @Suppress
+    @MediumTest
+    public void testOpenHangoutsFromHotseat() {
+        hotseatHelper.launchAppFromHotseat("Hangouts", "com.google.android.gms");
+    }
+
+    @Suppress
+    @MediumTest
+    public void testOpenPhotosFromHotseat() {
+        hotseatHelper.launchAppFromHotseat("Photos", "com.google.android.apps.photos");
+    }
+
+    @Suppress
+    @MediumTest
+    public void testOpenYoutubeFromHotseat() {
+        hotseatHelper.launchAppFromHotseat("YouTube", "com.google.android.youtube");
+    }
+
+    @Suppress
+    @MediumTest
+    public void testHomeToAllAppsNavigation() {
+        hotseatHelper.launchAppFromHotseat("Apps", getLauncherPackage());
+        assertNotNull("All apps page not found when navigating from hotseat",
+                mDevice.wait(Until.hasObject(By.res(getLauncherPackage(), "apps_view")), TIMEOUT));
+    }
+}
diff --git a/tests/functional/notificationtests/Android.mk b/tests/functional/notificationtests/Android.mk
new file mode 100644
index 0000000..de2ea8b
--- /dev/null
+++ b/tests/functional/notificationtests/Android.mk
@@ -0,0 +1,29 @@
+# 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.
+
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_PACKAGE_NAME := NotificationFunctionalTests
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_MODULE_TAGS := tests
+LOCAL_STATIC_JAVA_LIBRARIES := \
+    android-support-test \
+    launcher-helper-lib \
+    ub-uiautomator \
+    services.core
+
+#LOCAL_SDK_VERSION := current
+
+include $(BUILD_PACKAGE)
diff --git a/tests/functional/notificationtests/AndroidManifest.xml b/tests/functional/notificationtests/AndroidManifest.xml
new file mode 100644
index 0000000..970ac39
--- /dev/null
+++ b/tests/functional/notificationtests/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.notification.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.ACCESS_NOTIFICATION_POLICY" />
+    <uses-permission android:name="com.android.alarm.permission.SET_ALARM" />
+
+    <instrumentation
+        android:name="android.support.test.runner.AndroidJUnitRunner"
+        android:label="Android Notifications Functional Tests"
+        android:targetPackage="com.android.notification.functional" />
+
+</manifest>
diff --git a/tests/functional/notificationtests/res/drawable-xhdpi/stat_notify_email.png b/tests/functional/notificationtests/res/drawable-xhdpi/stat_notify_email.png
new file mode 100644
index 0000000..23c4672
--- /dev/null
+++ b/tests/functional/notificationtests/res/drawable-xhdpi/stat_notify_email.png
Binary files differ
diff --git a/tests/functional/notificationtests/src/com/android/notification/functional/HeadsUpNotificationTests.java b/tests/functional/notificationtests/src/com/android/notification/functional/HeadsUpNotificationTests.java
new file mode 100644
index 0000000..31b4c35
--- /dev/null
+++ b/tests/functional/notificationtests/src/com/android/notification/functional/HeadsUpNotificationTests.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 com.android.notification.functional;
+
+import android.app.NotificationManager;
+import android.content.Context;
+import android.content.Intent;
+import android.provider.AlarmClock;
+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.LargeTest;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.view.inputmethod.InputMethodManager;
+
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+
+public class HeadsUpNotificationTests extends InstrumentationTestCase {
+    private static final int SHORT_TIMEOUT = 1000;
+    private static final int LONG_TIMEOUT = 2000;
+    private static final int NOTIFICATION_ID_1 = 1;
+    private static final int NOTIFICATION_ID_2 = 2;
+    private static final String NOTIFICATION_CONTENT_TEXT = "INLINE REPLY TEST";
+    private NotificationManager mNotificationManager;
+    private UiDevice mDevice = null;
+    private Context mContext;
+    private NotificationHelper mHelper;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mDevice = UiDevice.getInstance(getInstrumentation());
+        mContext = getInstrumentation().getContext();
+        mNotificationManager = (NotificationManager) mContext
+                .getSystemService(Context.NOTIFICATION_SERVICE);
+        mHelper = new NotificationHelper(mDevice, getInstrumentation(), mNotificationManager);
+        mDevice.setOrientationNatural();
+        mHelper.unlockScreen();
+        mDevice.pressHome();
+        mNotificationManager.cancelAll();
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        mNotificationManager.cancelAll();
+        mDevice.pressHome();
+        mDevice.unfreezeRotation();
+        super.tearDown();
+    }
+
+    @MediumTest
+    public void testHeadsUpNotificationInlineReply() throws Exception {
+        mHelper.sendNotificationsWithInLineReply(NOTIFICATION_ID_1, true);
+        Thread.sleep(SHORT_TIMEOUT);
+        mDevice.wait(Until.findObject(By.text("REPLY")), LONG_TIMEOUT).click();
+        try {
+            UiObject2 replyBox = mDevice.wait(
+                    Until.findObject(By.res("com.android.systemui:id/remote_input_send")),
+                    LONG_TIMEOUT);
+            InputMethodManager imm = (InputMethodManager) mContext
+                    .getSystemService(Context.INPUT_METHOD_SERVICE);
+            if (!imm.isAcceptingText()) {
+                assertNotNull("Keyboard for inline reply has not loaded correctly", replyBox);
+            }
+        } finally {
+            mDevice.pressBack();
+        }
+    }
+
+    @MediumTest
+    public void testHeadsUpNotificationManualDismiss() throws Exception {
+        mHelper.sendNotificationsWithInLineReply(NOTIFICATION_ID_1, true);
+        Thread.sleep(SHORT_TIMEOUT);
+        UiObject2 obj = mDevice.wait(Until.findObject(By.text(NOTIFICATION_CONTENT_TEXT)),
+                LONG_TIMEOUT);
+        obj.swipe(Direction.LEFT, 1.0f);
+        Thread.sleep(SHORT_TIMEOUT);
+        if (mHelper.checkNotificationExistence(NOTIFICATION_ID_1, true)) {
+            fail(String.format("Notification %s has not been auto dismissed", NOTIFICATION_ID_1));
+        }
+    }
+
+    @LargeTest
+    public void testHeadsUpNotificationAutoDismiss() throws Exception {
+        mHelper.sendNotificationsWithInLineReply(NOTIFICATION_ID_1, true);
+        Thread.sleep(LONG_TIMEOUT * 3);
+        UiObject2 obj = mDevice.wait(Until.findObject(By.text(NOTIFICATION_CONTENT_TEXT)),
+                LONG_TIMEOUT);
+        assertNull(String.format("Notification %s has not been auto dismissed", NOTIFICATION_ID_1),
+                obj);
+    }
+
+    @MediumTest
+    public void testHeadsUpNotificationInlineReplyMulti() throws Exception {
+        mHelper.sendNotificationsWithInLineReply(NOTIFICATION_ID_1, true);
+        Thread.sleep(LONG_TIMEOUT);
+        mDevice.wait(Until.findObject(By.text("REPLY")), LONG_TIMEOUT).click();
+        UiObject2 replyBox = mDevice.wait(
+                Until.findObject(By.res("com.android.systemui:id/remote_input_send")),
+                LONG_TIMEOUT);
+        InputMethodManager imm = (InputMethodManager) mContext
+                .getSystemService(Context.INPUT_METHOD_SERVICE);
+        if (!imm.isAcceptingText()) {
+            assertNotNull("Keyboard for inline reply has not loaded correctly", replyBox);
+        }
+        mHelper.sendNotificationsWithInLineReply(NOTIFICATION_ID_2, true);
+        Thread.sleep(LONG_TIMEOUT);
+        UiObject2 obj = mDevice.wait(Until.findObject(By.text(NOTIFICATION_CONTENT_TEXT)),
+                LONG_TIMEOUT);
+        if (obj == null) {
+            assertNull(String.format("Notification %s can not be found", NOTIFICATION_ID_1),
+                    obj);
+        }
+    }
+
+    @LargeTest
+    public void testAlarm() throws Exception {
+        try {
+            setAlarmNow();
+            UiObject2 obj = mDevice.wait(Until.findObject(By.text("test")), 60000);
+            if (obj == null) {
+                fail("Alarm heads up notifcation is not working");
+            }
+        } finally {
+            mDevice.wait(Until.findObject(By.text("DISMISS")), LONG_TIMEOUT).click();
+        }
+    }
+
+    private void setAlarmNow() throws InterruptedException {
+        GregorianCalendar cal = new GregorianCalendar();
+        cal.setTimeInMillis(System.currentTimeMillis());
+        int hour = cal.get(Calendar.HOUR_OF_DAY);
+        int minute = cal.get(Calendar.MINUTE) + 1;// to make sure it won't be set at the next day
+        Intent intent = new Intent(AlarmClock.ACTION_SET_ALARM);
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        intent.putExtra(AlarmClock.EXTRA_HOUR, hour);
+        intent.putExtra(AlarmClock.EXTRA_MINUTES, minute);
+        intent.putExtra(AlarmClock.EXTRA_SKIP_UI, true);
+        intent.putExtra(AlarmClock.EXTRA_MESSAGE, "test");
+        mContext.startActivity(intent);
+        Thread.sleep(LONG_TIMEOUT * 2);
+    }
+}
diff --git a/tests/functional/notificationtests/src/com/android/notification/functional/NotificationBundlingTests.java b/tests/functional/notificationtests/src/com/android/notification/functional/NotificationBundlingTests.java
new file mode 100644
index 0000000..84e3f57
--- /dev/null
+++ b/tests/functional/notificationtests/src/com/android/notification/functional/NotificationBundlingTests.java
@@ -0,0 +1,128 @@
+/*
+ * 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.notification.functional;
+
+import android.app.NotificationManager;
+import android.content.Context;
+import android.os.RemoteException;
+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.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class NotificationBundlingTests extends InstrumentationTestCase {
+    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 String BUNDLE_GROUP_KEY = "group_key ";
+    private NotificationManager mNotificationManager;
+    private UiDevice mDevice = null;
+    private Context mContext;
+    private NotificationHelper mHelper;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mDevice = UiDevice.getInstance(getInstrumentation());
+        mContext = getInstrumentation().getContext();
+        mNotificationManager = (NotificationManager) mContext
+                .getSystemService(Context.NOTIFICATION_SERVICE);
+        mHelper = new NotificationHelper(mDevice, getInstrumentation(), mNotificationManager);
+        mDevice.pressHome();
+        mNotificationManager.cancelAll();
+        try {
+            mDevice.setOrientationNatural();
+        } catch (RemoteException e) {
+            throw new RuntimeException("failed to freeze device orientaion", e);
+        }
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        mNotificationManager.cancelAll();
+        mDevice.pressHome();
+        super.tearDown();
+    }
+
+    @MediumTest
+    public void testBundlingNotification() throws Exception {
+        List<Integer> lists = new ArrayList<Integer>(Arrays.asList(GROUP_NOTIFICATION_ID,
+                CHILD_NOTIFICATION_ID, SECOND_CHILD_NOTIFICATION_ID));
+        mHelper.sendBundlingNotifications(lists, BUNDLE_GROUP_KEY);
+        Thread.sleep(SHORT_TIMEOUT);
+        mHelper.swipeDown();
+        UiObject2 obj = mDevice.wait(
+                Until.findObject(By.res("com.android.systemui", "notification_title")),
+                LONG_TIMEOUT);
+        int currentY = obj.getVisibleCenter().y;
+        mDevice.wait(Until.findObject(By.res("android:id/expand_button")), LONG_TIMEOUT).click();
+        obj = mDevice.wait(Until.findObject(By.textContains(lists.get(1).toString())),
+                LONG_TIMEOUT);
+        assertFalse("The notifications have not been bundled",
+                obj.getVisibleCenter().y == currentY);
+    }
+
+    @MediumTest
+    public void testDismissBundlingNotification() throws Exception {
+        List<Integer> lists = new ArrayList<Integer>(Arrays.asList(GROUP_NOTIFICATION_ID,
+                CHILD_NOTIFICATION_ID, SECOND_CHILD_NOTIFICATION_ID));
+        mHelper.sendBundlingNotifications(lists, BUNDLE_GROUP_KEY);
+        mHelper.swipeDown();
+        dismissObject(Integer.toString(CHILD_NOTIFICATION_ID));
+        Thread.sleep(LONG_TIMEOUT);
+        for (int n : lists) {
+            if (mHelper.checkNotificationExistence(n, true)) {
+                fail(String.format("Notification %s has not been dismissed", n));
+            }
+        }
+    }
+
+    @MediumTest
+    public void testDismissIndividualNotification() throws Exception {
+        List<Integer> lists = new ArrayList<Integer>(Arrays.asList(GROUP_NOTIFICATION_ID,
+                CHILD_NOTIFICATION_ID, SECOND_CHILD_NOTIFICATION_ID));
+        mHelper.sendBundlingNotifications(lists, BUNDLE_GROUP_KEY);
+        Thread.sleep(SHORT_TIMEOUT);
+        mDevice.openNotification();
+        mDevice.wait(Until.findObject(By.res("android:id/expand_button")), LONG_TIMEOUT).click();
+        dismissObject(Integer.toString(CHILD_NOTIFICATION_ID));
+        Thread.sleep(LONG_TIMEOUT);
+        if (mHelper.checkNotificationExistence(CHILD_NOTIFICATION_ID, true)) {
+            fail(String.format("Notification %s has not been dismissed", CHILD_NOTIFICATION_ID));
+        }
+        if (mHelper.checkNotificationExistence(GROUP_NOTIFICATION_ID, false)) {
+            fail(String.format("Notification %s has been dismissed ", GROUP_NOTIFICATION_ID));
+        }
+    }
+
+    private void dismissObject(String text) {
+        UiObject2 obj = mDevice.wait(
+                Until.findObject(By.textContains(text)),
+                LONG_TIMEOUT);
+        int y = obj.getVisibleBounds().centerY();
+        mDevice.swipe(0, y, mDevice.getDisplayWidth(),
+                y, 5);
+    }
+}
diff --git a/tests/functional/notificationtests/src/com/android/notification/functional/NotificationDNDTests.java b/tests/functional/notificationtests/src/com/android/notification/functional/NotificationDNDTests.java
new file mode 100644
index 0000000..16fd608
--- /dev/null
+++ b/tests/functional/notificationtests/src/com/android/notification/functional/NotificationDNDTests.java
@@ -0,0 +1,178 @@
+
+package com.android.notification.functional;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.provider.Settings.Global;
+import android.provider.Settings.SettingNotFoundException;
+import android.service.notification.StatusBarNotification;
+import android.service.notification.ZenModeConfig;
+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.Until;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.util.Log;
+
+import com.android.server.notification.NotificationManagerService;
+import com.android.server.notification.ConditionProviders;
+import com.android.server.notification.ManagedServices.UserProfiles;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.FutureTask;
+
+import com.android.server.notification.ZenModeHelper;
+import com.android.server.notification.NotificationRecord;
+import com.android.server.notification.ZenModeFiltering;
+
+public class NotificationDNDTests extends InstrumentationTestCase {
+    private static final String LOG_TAG = NotificationDNDTests.class.getSimpleName();
+    private static final int SHORT_TIMEOUT = 1000;
+    private static final int LONG_TIMEOUT = 2000;
+    private static final int NOTIFICATION_ID = 1;
+    private static final String NOTIFICATION_CONTENT_TEXT = "INLINE REPLY TEST";
+    private NotificationManager mNotificationManager;
+    private UiDevice mDevice = null;
+    private Context mContext;
+    private NotificationHelper mHelper;
+    private ContentResolver mResolver;
+    private ZenModeHelper mZenHelper;
+    private boolean isGranted = false;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mDevice = UiDevice.getInstance(getInstrumentation());
+        mContext = getInstrumentation().getTargetContext();
+        mNotificationManager = (NotificationManager) mContext
+                .getSystemService(Context.NOTIFICATION_SERVICE);
+        mHelper = new NotificationHelper(mDevice, getInstrumentation(), mNotificationManager);
+        mResolver = mContext.getContentResolver();
+        ConditionProviders cps = new ConditionProviders(
+                mContext, new Handler(Looper.getMainLooper()),
+                new UserProfiles());
+        Callable<ZenModeHelper> callable = new Callable<ZenModeHelper>() {
+            @Override
+            public ZenModeHelper call() throws Exception {
+                return new ZenModeHelper(mContext, Looper.getMainLooper(), cps);
+            }
+        };
+        FutureTask<ZenModeHelper> task = new FutureTask<>(callable);
+        getInstrumentation().runOnMainSync(task);
+        mZenHelper = task.get();
+        mDevice.setOrientationNatural();
+        mHelper.unlockScreen();
+        mDevice.pressHome();
+        mNotificationManager.cancelAll();
+        isGranted = mNotificationManager.isNotificationPolicyAccessGranted();
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        mNotificationManager.cancelAll();
+        mDevice.pressHome();
+        mDevice.unfreezeRotation();
+        super.tearDown();
+    }
+
+    @LargeTest
+    public void testDND() throws Exception {
+        if (!isGranted) {
+            grantPolicyAccess(true);
+        }
+        int setting = mNotificationManager.getCurrentInterruptionFilter();
+        try {
+            mNotificationManager
+                    .setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY);
+            mHelper.sendNotificationsWithInLineReply(NOTIFICATION_ID, true);
+            Thread.sleep(LONG_TIMEOUT);
+            NotificationRecord nr = new NotificationRecord(mContext,
+                    mHelper.getStatusBarNotification(NOTIFICATION_ID));
+            ZenModeConfig mConfig = mZenHelper.getConfig();
+            ZenModeFiltering zF = new ZenModeFiltering(mContext);
+            assertTrue(zF.shouldIntercept(mNotificationManager.getZenMode(), mConfig, nr));
+        } finally {
+            mNotificationManager.setInterruptionFilter(setting);
+            if (!isGranted) {
+                grantPolicyAccess(false);
+            }
+        }
+    }
+
+    @LargeTest
+    public void testPriority() throws Exception {
+        int setting = mNotificationManager.getCurrentInterruptionFilter();
+        if (!isGranted) {
+            grantPolicyAccess(true);
+        }
+        mNotificationManager
+                .setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY);
+        try {
+            mHelper.showInstalledAppDetails(mContext, "com.android.notification.functional");
+            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));
+            ZenModeConfig mConfig = mZenHelper.getConfig();
+            ZenModeFiltering zF = new ZenModeFiltering(mContext);
+            assertFalse(zF.shouldIntercept(mZenHelper.getZenMode(), mConfig, nr));
+        } finally {
+            mHelper.showInstalledAppDetails(mContext, "com.android.notification.functional");
+            mDevice.wait(Until.findObject(By.textContains("Override Do Not Disturb")), LONG_TIMEOUT)
+                    .click();
+            mNotificationManager.setInterruptionFilter(setting);
+            if (!isGranted) {
+                grantPolicyAccess(false);
+            }
+        }
+    }
+
+    @LargeTest
+    public void testBlockNotification() throws Exception {
+        try {
+            mHelper.showInstalledAppDetails(mContext, "com.android.notification.functional");
+            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));
+            }
+        } finally {
+            mHelper.showInstalledAppDetails(mContext, "com.android.notification.functional");
+            mDevice.wait(Until.findObject(By.textContains("Block all")), LONG_TIMEOUT).click();
+        }
+    }
+
+    private void grantPolicyAccess(boolean isGranted) throws Exception {
+        NotificationHelper.launchSettingsPage(mContext,
+                android.provider.Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS);
+        Thread.sleep(LONG_TIMEOUT);
+        if (isGranted) {
+            mDevice.wait(Until.findObject(By.text("OFF")), LONG_TIMEOUT).click();
+            Thread.sleep(SHORT_TIMEOUT);
+            mDevice.wait(Until.findObject(By.text("ALLOW")), LONG_TIMEOUT).click();
+        } else {
+            mDevice.wait(Until.findObject(By.text("ON")), LONG_TIMEOUT).click();
+            Thread.sleep(SHORT_TIMEOUT);
+            mDevice.wait(Until.findObject(By.text("OK")), LONG_TIMEOUT).click();
+        }
+        Thread.sleep(LONG_TIMEOUT);
+        mDevice.pressHome();
+    }
+}
diff --git a/tests/functional/notificationtests/src/com/android/notification/functional/NotificationHelper.java b/tests/functional/notificationtests/src/com/android/notification/functional/NotificationHelper.java
new file mode 100644
index 0000000..7e36fa8
--- /dev/null
+++ b/tests/functional/notificationtests/src/com/android/notification/functional/NotificationHelper.java
@@ -0,0 +1,383 @@
+/*
+ * 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.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.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;
+import android.support.test.uiautomator.UiScrollable;
+import android.support.test.uiautomator.UiSelector;
+import android.support.test.uiautomator.Until;
+import android.text.SpannableStringBuilder;
+import android.text.style.StyleSpan;
+import android.util.Log;
+import android.widget.EditText;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.notification.functional.R;
+
+import java.lang.InterruptedException;
+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 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);
+
+    private UiDevice mDevice;
+    private Instrumentation mInst;
+    private NotificationManager mNotificationManager = null;
+    private Context mContext = null;
+
+    public NotificationHelper(UiDevice device, Instrumentation inst, NotificationManager nm) {
+        this.mDevice = device;
+        mInst = inst;
+        mNotificationManager = nm;
+        mContext = inst.getContext();
+    }
+
+    public void sleepAndWakeUpDevice() throws RemoteException, InterruptedException {
+        mDevice.sleep();
+        Thread.sleep(LONG_TIMEOUT);
+        mDevice.wakeUp();
+    }
+
+    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(LONG_TIMEOUT * 2);
+    }
+
+    /**
+     * Sets the screen lock pin
+     * @param pin 4 digits
+     * @return false if a pin is already set or pin value is not 4 digits
+     * @throws UiObjectNotFoundException
+     */
+    public boolean setScreenLockPin(int pin) throws Exception {
+        if (pin >= 0 && pin <= 9999) {
+            navigateToScreenLock();
+            if (new UiObject(new UiSelector().text("Confirm your PIN")).exists()) {
+                UiObject pinField = new UiObject(
+                        new UiSelector().className(EditText.class.getName()));
+                pinField.setText(String.format("%04d", pin));
+                mDevice.pressEnter();
+            }
+            new UiObject(new UiSelector().text("PIN")).click();
+            clickText("No thanks");
+            UiObject pinField = new UiObject(new UiSelector().className(EditText.class.getName()));
+            pinField.setText(String.format("%04d", pin));
+            mDevice.pressEnter();
+            pinField.setText(String.format("%04d", pin));
+            mDevice.pressEnter();
+            clickText("Hide sensitive notification content");
+            clickText("DONE");
+            return true;
+        }
+        return false;
+    }
+
+    public boolean removeScreenLock(int pin, String mode) throws Exception {
+        navigateToScreenLock();
+        if (new UiObject(new UiSelector().text("Confirm your PIN")).exists()) {
+            UiObject pinField = new UiObject(new UiSelector().className(EditText.class.getName()));
+            pinField.setText(String.format("%04d", pin));
+            mDevice.pressEnter();
+            clickText(mode);
+            clickText("YES, REMOVE");
+        } else {
+            clickText(mode);
+        }
+        return true;
+    }
+
+    public void unlockScreenByPin(int pin) throws Exception {
+        String command = String.format(" %s %s %s", "input", "text", Integer.toString(pin));
+        executeAdbCommand(command);
+        Thread.sleep(SHORT_TIMEOUT);
+        mDevice.pressEnter();
+    }
+
+    public void enableNotificationViaAdb(boolean isShow) {
+        String command = String.format(" %s %s %s %s %s", "settings", "put", "secure",
+                "lock_screen_show_notifications",
+                isShow ? "1" : "0");
+        executeAdbCommand(command);
+    }
+
+    public void executeAdbCommand(String command) {
+        Log.i(LOG_TAG, String.format("executing - %s", command));
+        mInst.getUiAutomation().executeShellCommand(command);
+        mDevice.waitForIdle();
+    }
+
+    private void navigateToScreenLock() throws Exception {
+        launchSettingsPage(mInst.getContext(), Settings.ACTION_SECURITY_SETTINGS);
+        new UiObject(new UiSelector().text("Screen lock")).click();
+    }
+
+    private void clickText(String text) throws UiObjectNotFoundException {
+        mDevice.wait(Until.findObject(By.text(text)), LONG_TIMEOUT).click();
+    }
+
+    public void sendNotification(int id, int visibility, String title) 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());
+        Notification notification = new Notification.Builder(mContext)
+                .setSmallIcon(R.drawable.stat_notify_email)
+                .setWhen(System.currentTimeMillis()).setContentTitle(title).setContentText(subtitle)
+                .setContentIntent(pendingIntent).setVisibility(visibility)
+                .setPriority(Notification.PRIORITY_HIGH)
+                .build();
+        mNotificationManager.notify(id, notification);
+        Thread.sleep(LONG_TIMEOUT);
+    }
+
+    public void sendNotifications(Map<Integer, String> lists) 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)
+                    .setSmallIcon(R.drawable.stat_notify_email)
+                    .setWhen(System.currentTimeMillis()).setContentTitle(l.getValue())
+                    .setContentText(subtitle)
+                    .build();
+            mNotificationManager.notify(l.getKey(), notification);
+        }
+        Thread.sleep(LONG_TIMEOUT);
+    }
+
+    public void sendBundlingNotifications(List<Integer> lists, String groupKey) throws Exception {
+        Notification childNotification = new Notification.Builder(mContext)
+                .setContentTitle(lists.get(1).toString())
+                .setSmallIcon(R.drawable.stat_notify_email)
+                .setGroup(groupKey)
+                .build();
+        mNotificationManager.notify(lists.get(1),
+                childNotification);
+        childNotification = new Notification.Builder(mContext)
+                .setContentText(lists.get(2).toString())
+                .setSmallIcon(R.drawable.stat_notify_email)
+                .setGroup(groupKey)
+                .build();
+        mNotificationManager.notify(lists.get(2),
+                childNotification);
+        Notification notification = new Notification.Builder(mContext)
+                .setContentTitle(lists.get(0).toString())
+                .setSubText(groupKey)
+                .setSmallIcon(R.drawable.stat_notify_email)
+                .setGroup(groupKey)
+                .setGroupSummary(true)
+                .build();
+        mNotificationManager.notify(lists.get(0),
+                notification);
+    }
+
+    static SpannableStringBuilder BOLD(CharSequence str) {
+        final SpannableStringBuilder ssb = new SpannableStringBuilder(str);
+        ssb.setSpan(new StyleSpan(Typeface.BOLD), 0, ssb.length(), 0);
+        return ssb;
+    }
+
+    public boolean checkNotificationExistence(int id, boolean exists) 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 == exists) {
+                break;
+            }
+            Thread.sleep(SHORT_TIMEOUT);
+        }
+        Log.i(LOG_TAG, "checkNotificationExistence..." + isFound);
+        return isFound == exists;
+    }
+
+    public StatusBarNotification getStatusBarNotification(int id) {
+        StatusBarNotification[] sbns = mNotificationManager.getActiveNotifications();
+        StatusBarNotification n = null;
+        for (StatusBarNotification sbn : sbns) {
+            if (sbn.getId() == id) {
+                n = sbn;
+                break;
+            }
+        }
+        return n;
+    }
+
+    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 unlockScreen() throws Exception {
+        KeyguardManager myKM = (KeyguardManager) mContext
+                .getSystemService(Context.KEYGUARD_SERVICE);
+        if (myKM.inKeyguardRestrictedInputMode()) {
+            // it is locked
+            swipeUp();
+        }
+    }
+
+    public void showInstalledAppDetails(Context context, String packageName) throws Exception {
+        Intent intent = new Intent();
+        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);
+        context.startActivity(intent);
+        Thread.sleep(LONG_TIMEOUT * 2);
+    }
+
+    /**
+     * This is the main list view containing the items that settings are possible for
+     */
+    public static class SettingsListView {
+        public static boolean selectSettingsFor(String name) throws UiObjectNotFoundException {
+            UiScrollable settingsList = new UiScrollable(
+                    new UiSelector().resourceId("android:id/content"));
+            UiObject appSettings = settingsList.getChildByText(LIST_ITEM_VALUE, name);
+            if (appSettings != null) {
+                return appSettings.click();
+            }
+            return false;
+        }
+
+        public boolean checkSettingsExists(String name) {
+            try {
+                UiScrollable settingsList = new UiScrollable(LIST_VIEW);
+                UiObject appSettings = settingsList.getChildByText(LIST_ITEM_VALUE, name);
+                return appSettings.exists();
+            } catch (UiObjectNotFoundException e) {
+                return false;
+            }
+        }
+    }
+
+    public void sendNotificationsWithInLineReply(int notificationId, boolean isHeadsUp) {
+        Notification.Action action = new Notification.Action.Builder(
+                R.drawable.stat_notify_email, "Reply", ToastService.getPendingIntent(mContext,
+                        "inline reply test"))
+                                .addRemoteInput(new RemoteInput.Builder(KEY_QUICK_REPLY_TEXT)
+                                        .setLabel("Quick reply").build())
+                                .build();
+        Notification.Builder n = new Notification.Builder(mContext)
+                .setContentTitle(Integer.toString(notificationId))
+                .setContentText("INLINE REPLY TEST")
+                .setWhen(System.currentTimeMillis())
+                .setSmallIcon(R.drawable.stat_notify_email)
+                .addAction(action);
+        if (isHeadsUp) {
+            n.setPriority(Notification.PRIORITY_HIGH)
+                    .setDefaults(Notification.DEFAULT_VIBRATE);
+        }
+        mNotificationManager.notify(notificationId, n.build());
+    }
+
+    public static class ToastService extends IntentService {
+        private static final String TAG = "ToastService";
+        private static final String ACTION_TOAST = "toast";
+        private Handler handler;
+
+        public ToastService() {
+            super(TAG);
+        }
+
+        public ToastService(String name) {
+            super(name);
+        }
+
+        @Override
+        public int onStartCommand(Intent intent, int flags, int startId) {
+            handler = new Handler();
+            return super.onStartCommand(intent, flags, startId);
+        }
+
+        @Override
+        protected void onHandleIntent(Intent intent) {
+            if (intent.hasExtra("text")) {
+                final String text = intent.getStringExtra("text");
+                handler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        Toast.makeText(ToastService.this, text, Toast.LENGTH_LONG).show();
+                        Log.v(TAG, "toast " + text);
+                    }
+                });
+            }
+        }
+
+        public static PendingIntent getPendingIntent(Context context, String text) {
+            Intent toastIntent = new Intent(context, ToastService.class);
+            toastIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            toastIntent.setAction(ACTION_TOAST + ":" + text); // one per toast message
+            toastIntent.putExtra("text", text);
+            PendingIntent pi = PendingIntent.getService(
+                    context, 58, toastIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+            return pi;
+        }
+    }
+}
diff --git a/tests/functional/notificationtests/src/com/android/notification/functional/NotificationInlineReplyTests.java b/tests/functional/notificationtests/src/com/android/notification/functional/NotificationInlineReplyTests.java
new file mode 100644
index 0000000..84f3c49
--- /dev/null
+++ b/tests/functional/notificationtests/src/com/android/notification/functional/NotificationInlineReplyTests.java
@@ -0,0 +1,103 @@
+/*
+ * 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.notification.functional;
+
+import android.app.NotificationManager;
+import android.content.Context;
+import android.os.RemoteException;
+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.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.view.inputmethod.InputMethodManager;
+
+public class NotificationInlineReplyTests extends InstrumentationTestCase {
+    private static final int SHORT_TIMEOUT = 200;
+    private static final int LONG_TIMEOUT = 2000;
+    private static final int NOTIFICATION_ID = 1;
+    private NotificationManager mNotificationManager;
+    private UiDevice mDevice = null;
+    private Context mContext;
+    private NotificationHelper mHelper;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mDevice = UiDevice.getInstance(getInstrumentation());
+        mContext = getInstrumentation().getContext();
+        mNotificationManager = (NotificationManager) mContext
+                .getSystemService(Context.NOTIFICATION_SERVICE);
+        mHelper = new NotificationHelper(mDevice, getInstrumentation(), mNotificationManager);
+        try {
+            mDevice.setOrientationNatural();
+        } catch (RemoteException e) {
+            throw new RuntimeException("failed to freeze device orientaion", e);
+        }
+        mDevice.pressHome();
+        mNotificationManager.cancelAll();
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        mNotificationManager.cancelAll();
+        mDevice.pressHome();
+        super.tearDown();
+    }
+
+    @MediumTest
+    public void testInLineNotificationWithLockScreen() throws Exception {
+        try {
+            mHelper.sendNotificationsWithInLineReply(NOTIFICATION_ID,false);
+            mHelper.sleepAndWakeUpDevice();
+            UiObject2 obj = mDevice.wait(Until.findObject(By.text("INLINE REPLY TEST")),
+                    LONG_TIMEOUT);
+            mDevice.swipe(obj.getVisibleBounds().centerX(), obj.getVisibleBounds().centerY(),
+                    obj.getVisibleBounds().centerX(),
+                    mDevice.getDisplayHeight(), 5);
+            mDevice.wait(Until.findObject(By.text("REPLY")), LONG_TIMEOUT).click();
+            UiObject2 replyBox = mDevice.wait(
+                    Until.findObject(By.res("com.android.systemui:id/remote_input_send")),
+                    LONG_TIMEOUT);
+            InputMethodManager imm = (InputMethodManager) mContext
+                    .getSystemService(Context.INPUT_METHOD_SERVICE);
+            if (!imm.isAcceptingText()) {
+                assertNotNull("Keyboard for inline reply has not loaded correctly", replyBox);
+            }
+        } finally {
+            mHelper.swipeUp();
+        }
+    }
+
+    @MediumTest
+    public void testInLineNotificationsWithQuickSetting() throws Exception {
+        mHelper.sendNotificationsWithInLineReply(NOTIFICATION_ID,false);
+        Thread.sleep(SHORT_TIMEOUT);
+        mDevice.openQuickSettings();
+        mDevice.openNotification();
+        mDevice.wait(Until.findObject(By.text("REPLY")), LONG_TIMEOUT).click();
+        UiObject2 replyBox = mDevice.wait(
+                Until.findObject(By.res("com.android.systemui:id/remote_input_send")),
+                LONG_TIMEOUT);
+        InputMethodManager imm = (InputMethodManager) mContext
+                .getSystemService(Context.INPUT_METHOD_SERVICE);
+        if (!imm.isAcceptingText()) {
+            assertNotNull("Keyboard for inline reply has not loaded correctly", replyBox);
+        }
+    }
+}
diff --git a/tests/functional/notificationtests/src/com/android/notification/functional/NotificationInteractionTests.java b/tests/functional/notificationtests/src/com/android/notification/functional/NotificationInteractionTests.java
new file mode 100644
index 0000000..83aae52
--- /dev/null
+++ b/tests/functional/notificationtests/src/com/android/notification/functional/NotificationInteractionTests.java
@@ -0,0 +1,123 @@
+/*
+ * 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.notification.functional;
+
+import android.app.NotificationManager;
+import android.content.Context;
+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.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.util.Log;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class NotificationInteractionTests extends InstrumentationTestCase {
+    private static final String LOG_TAG = NotificationInteractionTests.class.getSimpleName();
+    private static final int LONG_TIMEOUT = 2000;
+    private final boolean DEBUG = false;
+    private NotificationManager mNotificationManager;
+    private UiDevice mDevice = null;
+    private Context mContext;
+    private NotificationHelper mHelper;
+    private static final int CUSTOM_NOTIFICATION_ID = 1;
+    private static final int NOTIFICATIONS_COUNT = 3;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mDevice = UiDevice.getInstance(getInstrumentation());
+        mContext = getInstrumentation().getContext();
+        mNotificationManager = (NotificationManager) mContext
+                .getSystemService(Context.NOTIFICATION_SERVICE);
+        mHelper = new NotificationHelper(mDevice, getInstrumentation(), mNotificationManager);
+        mDevice.setOrientationNatural();
+        mNotificationManager.cancelAll();
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        super.tearDown();
+        mDevice.unfreezeRotation();
+        mDevice.pressHome();
+        mNotificationManager.cancelAll();
+    }
+
+    @MediumTest
+    public void testNonDismissNotification() throws Exception {
+        String text = "USB debugging connected";
+        mDevice.openNotification();
+        Thread.sleep(LONG_TIMEOUT);
+        UiObject2 obj = findByText(text);
+        assertNotNull(String.format("Couldn't find %s notification", text), obj);
+        obj.swipe(Direction.LEFT, 1.0f);
+        Thread.sleep(LONG_TIMEOUT);
+        obj = mDevice.wait(Until.findObject(By.text(text)),
+                LONG_TIMEOUT);
+        assertNotNull("USB debugging notification has been dismissed", obj);
+    }
+
+    /** send out multiple notifications in order to test CLEAR ALL function */
+    @MediumTest
+    public void testDismissAll() throws Exception {
+        String text = "CLEAR ALL";
+        Map<Integer, String> lists = new HashMap<Integer, String>();
+        StatusBarNotification[] sbns = mNotificationManager.getActiveNotifications();
+        int currentSbns = sbns.length;
+        for (int i = 0; i < NOTIFICATIONS_COUNT; i++) {
+            lists.put(CUSTOM_NOTIFICATION_ID + i, Integer.toString(CUSTOM_NOTIFICATION_ID + i));
+        }
+        mHelper.sendNotifications(lists);
+        if (DEBUG) {
+            Log.d(LOG_TAG,
+                    String.format("posted %s notifications, here they are: ", NOTIFICATIONS_COUNT));
+            sbns = mNotificationManager.getActiveNotifications();
+            for (StatusBarNotification sbn : sbns) {
+                Log.d(LOG_TAG, "  " + sbn);
+            }
+        }
+        if (mDevice.openNotification()) {
+            Thread.sleep(LONG_TIMEOUT);
+            UiObject2 clearAll = findByText(text);
+            clearAll.click();
+        }
+        Thread.sleep(LONG_TIMEOUT);
+        sbns = mNotificationManager.getActiveNotifications();
+        assertTrue(String.format("%s notifications have not been cleared", sbns.length),
+                sbns.length == currentSbns);
+    }
+
+    private UiObject2 findByText(String text) throws Exception {
+        int maxAttempt = 5;
+        UiObject2 item = null;
+        while (maxAttempt-- > 0) {
+            item = mDevice.wait(Until.findObject(By.text(text)), LONG_TIMEOUT);
+            if (item == null) {
+                mDevice.swipe(mDevice.getDisplayWidth() / 2, mDevice.getDisplayHeight() / 2,
+                        mDevice.getDisplayWidth() / 2, 0, 30);
+            } else {
+                return item;
+            }
+        }
+        return null;
+    }
+}
diff --git a/tests/functional/notificationtests/src/com/android/notification/functional/NotificationSecurityLargeTests.java b/tests/functional/notificationtests/src/com/android/notification/functional/NotificationSecurityLargeTests.java
new file mode 100644
index 0000000..f9a36b0
--- /dev/null
+++ b/tests/functional/notificationtests/src/com/android/notification/functional/NotificationSecurityLargeTests.java
@@ -0,0 +1,86 @@
+/*
+ * 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.notification.functional;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Until;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+
+public class NotificationSecurityLargeTests extends InstrumentationTestCase {
+
+    private static final String LOG_TAG = NotificationSecurityLargeTests.class.getSimpleName();
+    private static final int SHORT_TIMEOUT = 200;
+    private static final int NOTIFICATION_ID_SECRET = 1;
+    private static final int NOTIFICATION_ID_PUBLIC = 2;
+    private static final int PIN = 1234;
+    private NotificationManager mNotificationManager = null;
+    private UiDevice mDevice = null;
+    private Context mContext;
+    private NotificationHelper mHelper;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mDevice = UiDevice.getInstance(getInstrumentation());
+        mContext = getInstrumentation().getContext();
+        mNotificationManager = (NotificationManager) mContext
+                .getSystemService(Context.NOTIFICATION_SERVICE);
+        Log.v(LOG_TAG, "set up notification...");
+        mHelper = new NotificationHelper(mDevice, getInstrumentation(), mNotificationManager);
+        mHelper.setScreenLockPin(PIN);
+        mHelper.sleepAndWakeUpDevice();
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        mHelper.swipeUp();
+        Thread.sleep(SHORT_TIMEOUT);
+        mHelper.unlockScreenByPin(PIN);
+        mHelper.removeScreenLock(PIN, "Swipe");
+        mNotificationManager.cancelAll();
+        super.tearDown();
+    }
+
+    @LargeTest
+    public void testVisibilitySecret() throws Exception {
+        String title = "Secret Title";
+        Log.i(LOG_TAG, "Begin test visibility equals VISIBILITY_SECRET ");
+        mHelper.sendNotification(NOTIFICATION_ID_SECRET, Notification.VISIBILITY_SECRET, title);
+        if (!mHelper.checkNotificationExistence(NOTIFICATION_ID_SECRET, true)) {
+            fail("couldn't find posted notification id=" + NOTIFICATION_ID_PUBLIC);
+        }
+        assertFalse(mDevice.wait(Until.hasObject(By.res("android:id/title").text(title)),
+                SHORT_TIMEOUT));
+    }
+
+    @LargeTest
+    public void testVisibilityPrivate() throws Exception {
+        Log.i(LOG_TAG, "Begin test visibility equals VISIBILITY_PRIVATE ");
+        mHelper.sendNotification(NOTIFICATION_ID_PUBLIC, Notification.VISIBILITY_PRIVATE, "");
+        if (!mHelper.checkNotificationExistence(NOTIFICATION_ID_PUBLIC, true)) {
+            fail("couldn't find posted notification id=" + NOTIFICATION_ID_PUBLIC);
+        }
+        assertNotNull(
+                Until.findObject(By.res("android:id/title").text("Contents hidden"))); 
+    }
+}
diff --git a/tests/functional/notificationtests/src/com/android/notification/functional/NotificationSecurityTests.java b/tests/functional/notificationtests/src/com/android/notification/functional/NotificationSecurityTests.java
new file mode 100644
index 0000000..80cdfd2
--- /dev/null
+++ b/tests/functional/notificationtests/src/com/android/notification/functional/NotificationSecurityTests.java
@@ -0,0 +1,75 @@
+/*
+ * 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.notification.functional;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.service.notification.StatusBarNotification;
+import android.support.test.uiautomator.UiDevice;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.util.Log;
+
+public class NotificationSecurityTests extends InstrumentationTestCase {
+
+    private static final String LOG_TAG = NotificationSecurityTests.class.getSimpleName();
+    private static final int NOTIFICATION_ID_PUBLIC = 1;
+    private NotificationManager mNotificationManager = null;
+    private UiDevice mDevice = null;
+    private Context mContext;
+    private NotificationHelper mHelper;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mDevice = UiDevice.getInstance(getInstrumentation());
+        mContext = getInstrumentation().getContext();
+        mNotificationManager = (NotificationManager) mContext
+                .getSystemService(Context.NOTIFICATION_SERVICE);
+        Log.i(LOG_TAG, "set up notification...");
+        mHelper = new NotificationHelper(mDevice, getInstrumentation(), mNotificationManager);
+        mDevice.freezeRotation();
+        mHelper.sleepAndWakeUpDevice();
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        mNotificationManager.cancelAll();
+        mHelper.swipeUp();
+        super.tearDown();
+    }
+
+    @MediumTest
+    public void testVisibilityPublic() throws Exception {
+        mHelper.enableNotificationViaAdb(true);
+        String title = "Public Notification";
+        Log.i(LOG_TAG, "Begin test visibility equals VISIBILITY_PUBLIC ");
+        mHelper.sendNotification(NOTIFICATION_ID_PUBLIC, Notification.VISIBILITY_PUBLIC, title);
+        if (mHelper.checkNotificationExistence(NOTIFICATION_ID_PUBLIC, false)) {
+            fail("couldn't find posted notification id=" + NOTIFICATION_ID_PUBLIC);
+        }
+        StatusBarNotification[] sbns = mNotificationManager.getActiveNotifications();
+        for (StatusBarNotification sbn : sbns) {
+            Log.i(LOG_TAG, sbn.getNotification().extras.getString(Notification.EXTRA_TITLE));
+            if (sbn.getId() == NOTIFICATION_ID_PUBLIC) {
+                String sentTitle = sbn.getNotification().extras.getString(Notification.EXTRA_TITLE);
+                assertEquals(sentTitle, title);
+            }
+        }
+    }
+}
diff --git a/tests/functional/otatests/Android.mk b/tests/functional/otatests/Android.mk
new file mode 100644
index 0000000..110ea6a
--- /dev/null
+++ b/tests/functional/otatests/Android.mk
@@ -0,0 +1,14 @@
+
+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 := \
+    easymocklib \
+    objenesis-target \
+    ub-uiautomator
+
+include $(BUILD_PACKAGE)
diff --git a/tests/functional/otatests/AndroidManifest.xml b/tests/functional/otatests/AndroidManifest.xml
new file mode 100644
index 0000000..54cccad
--- /dev/null
+++ b/tests/functional/otatests/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.functional.otatests">
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+
+    <uses-sdk android:minSdkVersion="19"
+        android:targetSdkVersion="24" />
+    <instrumentation
+        android:name="android.test.InstrumentationTestRunner"
+        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
new file mode 100644
index 0000000..cf74375
--- /dev/null
+++ b/tests/functional/otatests/src/com/android/functional/otatests/PackageProcessTest.java
@@ -0,0 +1,84 @@
+/*
+ * 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 android.content.Context;
+import android.content.ContextWrapper;
+import android.os.IPowerManager;
+import android.os.PowerManager;
+import android.os.RecoverySystem;
+import android.test.InstrumentationTestCase;
+
+import java.io.File;
+
+public class PackageProcessTest extends InstrumentationTestCase {
+
+    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 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);
+        }
+    }
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mContext = getInstrumentation().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);
+    }
+
+    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());
+    }
+
+    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/SystemUpdateTest.java b/tests/functional/otatests/src/com/android/functional/otatests/SystemUpdateTest.java
new file mode 100644
index 0000000..065ccb1
--- /dev/null
+++ b/tests/functional/otatests/src/com/android/functional/otatests/SystemUpdateTest.java
@@ -0,0 +1,11 @@
+package com.android.functional.otatests;
+
+/**
+ * A basic test case to assert that the system was updated to the expected version.
+ */
+public class SystemUpdateTest extends VersionCheckingTest {
+
+    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
new file mode 100644
index 0000000..174786c
--- /dev/null
+++ b/tests/functional/otatests/src/com/android/functional/otatests/VersionCheckingTest.java
@@ -0,0 +1,57 @@
+package com.android.functional.otatests;
+
+import android.test.InstrumentationTestCase;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+
+public class VersionCheckingTest extends InstrumentationTestCase {
+
+    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 VersionInfo mOldVersion;
+    protected VersionInfo mNewVersion;
+
+    @Override
+    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);
+        }
+    }
+
+    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
new file mode 100644
index 0000000..448464f
--- /dev/null
+++ b/tests/functional/otatests/src/com/android/functional/otatests/VersionInfo.java
@@ -0,0 +1,48 @@
+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/permission/Android.mk b/tests/functional/permission/Android.mk
new file mode 100644
index 0000000..0ecdeb2
--- /dev/null
+++ b/tests/functional/permission/Android.mk
@@ -0,0 +1,15 @@
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_SDK_VERSION := current
+
+LOCAL_SRC_FILES := $(call all-subdir-java-files)
+LOCAL_JAVA_LIBRARIES := android.test.runner
+LOCAL_STATIC_JAVA_LIBRARIES := ub-uiautomator launcher-helper-lib
+
+LOCAL_PACKAGE_NAME := PermissionFunctionalTests
+LOCAL_CERTIFICATE := platform
+
+include $(BUILD_PACKAGE)
diff --git a/tests/functional/permission/AndroidManifest.xml b/tests/functional/permission/AndroidManifest.xml
new file mode 100644
index 0000000..b13b888
--- /dev/null
+++ b/tests/functional/permission/AndroidManifest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 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.functional.permissiontests">
+
+    <uses-sdk android:minSdkVersion="23"
+              android:targetSdkVersion="23" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.WRITE_CONTACTS" />
+    <uses-permission android:name="android.permission.SEND_SMS" />
+    <uses-permission android:name="android.permission.RECEIVE_SMS" />
+    <application>
+        <uses-library android:name="android.test.runner"/>
+    </application>
+    <instrumentation
+            android:name="android.test.InstrumentationTestRunner"
+            android:targetPackage="com.android.functional.permissiontests"
+            android:label="Permission Functional Tests" />
+</manifest>
diff --git a/tests/functional/permission/src/com/android/functional/permissiontests/GenericAppPermissionTests.java b/tests/functional/permission/src/com/android/functional/permissiontests/GenericAppPermissionTests.java
new file mode 100644
index 0000000..1923e3c
--- /dev/null
+++ b/tests/functional/permission/src/com/android/functional/permissiontests/GenericAppPermissionTests.java
@@ -0,0 +1,139 @@
+/*
+ * 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.permissiontests;
+
+import android.app.UiAutomation;
+import android.content.Context;
+import android.os.RemoteException;
+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.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;
+
+public class GenericAppPermissionTests extends InstrumentationTestCase {
+    protected final String PACKAGE_INSTALLER = "com.android.packageinstaller";
+    protected final String TARGET_APP_PKG = "com.android.functional.permissiontests";
+    private final String PERMISSION_TEST_APP_PKG = "com.android.permissiontestappmv1";
+    private final String PERMISSION_TEST_APP = "PermissionTestAppMV1";
+    private UiDevice mDevice = null;
+    private Context mContext = null;
+    private UiAutomation mUiAutomation = null;
+    private PermissionHelper pHelper;
+    private final String[] mDefaultPermittedGroups = new String[] {
+            "CONTACTS", "SMS", "STORAGE"
+    };
+
+    private final String[] mDefaultPermittedDangerousPermissions = new String[] {
+            "android.permission.WRITE_EXTERNAL_STORAGE", "android.permission.WRITE_CONTACTS",
+            "android.permission.SEND_SMS", "android.permission.RECEIVE_SMS"
+    };
+
+    private List<String> mDefaultGrantedPermissions;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mDevice = UiDevice.getInstance(getInstrumentation());
+        mContext = getInstrumentation().getContext();
+        mUiAutomation = getInstrumentation().getUiAutomation();
+        mDevice.setOrientationNatural();
+        pHelper = PermissionHelper.getInstance(mDevice, mContext, mUiAutomation);
+        mDefaultGrantedPermissions = pHelper.getPermissionByPackage(TARGET_APP_PKG, Boolean.TRUE);
+    }
+
+    @SmallTest
+    public void testDefaultDangerousPermissionGranted() {
+        pHelper.verifyDefaultDangerousPermissionGranted(TARGET_APP_PKG,
+                mDefaultPermittedDangerousPermissions, Boolean.FALSE);
+    }
+
+    @SmallTest
+    public void testExtraDangerousPermissionNotGranted() {
+        pHelper.verifyExtraDangerousPermissionNotGranted(TARGET_APP_PKG, mDefaultPermittedGroups);
+    }
+
+    @SmallTest
+    public void testNormalPermissionsAutoGranted() {
+        pHelper.verifyNormalPermissionsAutoGranted(TARGET_APP_PKG);
+    }
+
+    public void testToggleAppPermisssionOFF() {
+        pHelper.togglePermissionSetting(PERMISSION_TEST_APP, "Contacts", Boolean.FALSE);
+        pHelper.verifyPermissionSettingStatus(
+                PERMISSION_TEST_APP, "Contacts", PermissionStatus.OFF);
+    }
+
+    public void testToggleAppPermisssionON() {
+        pHelper.togglePermissionSetting(PERMISSION_TEST_APP, "Contacts", Boolean.TRUE);
+        pHelper.verifyPermissionSettingStatus(PERMISSION_TEST_APP, "Contacts", PermissionStatus.ON);
+    }
+
+    @MediumTest
+    public void testViewPermissionDescription() {
+        List<String> permissionDescGrpNamesList = pHelper
+                .getPermissionDescGroupNames(TARGET_APP_PKG);
+        permissionDescGrpNamesList.removeAll(Arrays.asList(mDefaultPermittedGroups));
+        assertTrue("Still there are more", permissionDescGrpNamesList.isEmpty());
+    }
+
+    public void testPermissionDialogAllow() {
+        pHelper.cleanPackage(PERMISSION_TEST_APP_PKG);
+        pHelper.launchApp(PERMISSION_TEST_APP_PKG, PERMISSION_TEST_APP);
+        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();
+        assertTrue("Permission hasn't been granted",
+                pHelper.getAllDangerousPermissionsByPermGrpNames(
+                        new String[] {"Contacts"} ).size() > 0);
+    }
+
+    public void testPermissionDialogDenyFlow() {
+        pHelper.cleanPackage(PERMISSION_TEST_APP_PKG);
+        pHelper.launchApp(PERMISSION_TEST_APP_PKG, PERMISSION_TEST_APP);
+        pHelper.grantOrRevokePermissionViaAdb(
+                PERMISSION_TEST_APP_PKG, "android.permission.READ_CONTACTS", 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");
+
+        mDevice.wait(Until.findObject(getContactSelector), pHelper.TIMEOUT).click();
+        UiObject2 dontAskChk = mDevice.wait(Until.findObject(dontAskChkSelector), pHelper.TIMEOUT);
+        assertNull(dontAskChk);
+        mDevice.wait(Until.findObject(denySelctor), pHelper.TIMEOUT).click();
+        mDevice.wait(Until.findObject(getContactSelector), pHelper.TIMEOUT).click();
+        mDevice.wait(Until.findObject(dontAskChkSelector), pHelper.TIMEOUT).click();
+        mDevice.wait(Until.findObject(denySelctor), pHelper.TIMEOUT).click();
+        mDevice.wait(Until.findObject(getContactSelector), pHelper.TIMEOUT).click();;
+        assertNull(mDevice.wait(Until.findObject(denySelctor), pHelper.TIMEOUT));
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        mDevice.unfreezeRotation();
+        super.tearDown();
+    }
+}
diff --git a/tests/functional/permission/src/com/android/functional/permissiontests/PermissionHelper.java b/tests/functional/permission/src/com/android/functional/permissiontests/PermissionHelper.java
new file mode 100644
index 0000000..d6edd8a
--- /dev/null
+++ b/tests/functional/permission/src/com/android/functional/permissiontests/PermissionHelper.java
@@ -0,0 +1,430 @@
+/*
+ * 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.permissiontests;
+
+import android.app.UiAutomation;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.ParcelFileDescriptor;
+import android.os.SystemClock;
+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.Until;
+import android.util.Log;
+
+import junit.framework.Assert;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Hashtable;
+import java.util.List;
+
+public class PermissionHelper {
+    public static final String TEST_TAG = "PermissionTest";
+    public static final String SETTINGS_PACKAGE = "com.android.settings";
+    public final int TIMEOUT = 500;
+    public static PermissionHelper mInstance = null;
+    private UiDevice mDevice;
+    private Context mContext;
+    private static UiAutomation mUiAutomation;
+    public static Hashtable<String, List<String>> mPermissionGroupInfo = null;
+    ILauncherStrategy mLauncherStrategy;
+
+    private PermissionHelper(UiDevice device, Context context, UiAutomation uiAutomation) {
+        mDevice = device;
+        mContext = context;
+        mUiAutomation = uiAutomation;
+        mLauncherStrategy = LauncherStrategyFactory.getInstance(mDevice).getLauncherStrategy();
+    }
+
+    public static PermissionHelper getInstance(UiDevice device, Context context,
+            UiAutomation uiAutomation) {
+        if (mInstance == null) {
+            mInstance = new PermissionHelper(device, context, uiAutomation);
+            PermissionHelper.populateDangerousPermissionGroupInfo();
+        }
+        return mInstance;
+    }
+
+    /**
+     * Returns list of all dangerous permission of the system
+     */
+    private static void populateDangerousPermissionGroupInfo() {
+        ParcelFileDescriptor pfd = mUiAutomation.executeShellCommand("pm list permissions -g -d");
+        try (BufferedReader reader = new BufferedReader(
+                new InputStreamReader(new FileInputStream(pfd.getFileDescriptor())))) {
+            String line;
+            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 (mPermissionGroupInfo == null) {
+                        mPermissionGroupInfo = new Hashtable<String, List<String>>();
+                    } else {
+                        mPermissionGroupInfo.put(groupName, permissions);
+                        permissions = new ArrayList<String>();
+                    }
+                    groupName = line.split(":")[1];
+                } else if (line.startsWith("  permission:")) {
+                    permissions.add(line.split(":")[1]);
+                }
+            }
+            mPermissionGroupInfo.put(groupName, permissions);
+        } catch (IOException e) {
+            Log.e(TEST_TAG, e.getMessage());
+        }
+    }
+
+    /**
+     * Returns list of 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
+     * @return
+     */
+    public List<String> getPermissionByPackage(String packageName, Boolean permitted) {
+        List<String> selectedPermissions = new ArrayList<String>();
+        String[] requestedPermissions = null;
+        int[] requestedPermissionFlags = null;
+        PackageInfo packageInfo = null;
+        try {
+            packageInfo = getPackageManager().getPackageInfo(packageName,
+                    PackageManager.GET_PERMISSIONS);
+        } catch (NameNotFoundException e) {
+            throw new RuntimeException(String.format("%s package isn't found", packageName));
+        }
+
+        requestedPermissions = packageInfo.requestedPermissions;
+        requestedPermissionFlags = packageInfo.requestedPermissionsFlags;
+        for (int i = 0; i < requestedPermissions.length; ++i) {
+            // requestedPermissionFlags 1 = Denied, 3 = Granted
+            if (permitted && requestedPermissionFlags[i] == 3) {
+                selectedPermissions.add(requestedPermissions[i]);
+            } else if (!permitted && requestedPermissionFlags[i] == 1) {
+                selectedPermissions.add(requestedPermissions[i]);
+            }
+        }
+        return selectedPermissions;
+    }
+
+    /**
+     * Verify any dangerous permission not mentioned in manifest aren't granted
+     * @param packageName
+     * @param permittedGroups
+     */
+    public void verifyExtraDangerousPermissionNotGranted(String packageName,
+            String[] permittedGroups) {
+        List<String> allPermittedDangerousPermsList = getAllDangerousPermissionsByPermGrpNames(
+                permittedGroups);
+        List<String> allPermissionsForPackageList = getPermissionByPackage(packageName,
+                Boolean.TRUE);
+        List<String> allPlatformDangerousPermissionList = getPlatformDangerousPermissionGroupNames();
+        allPermissionsForPackageList.retainAll(allPlatformDangerousPermissionList);
+        allPermissionsForPackageList.removeAll(allPermittedDangerousPermsList);
+        Assert.assertTrue(
+                String.format("For package %s some extra dangerous permissions have been granted",
+                        packageName),
+                allPermissionsForPackageList.isEmpty());
+    }
+
+    /**
+     * Verify any dangerous permission mentioned in manifest that is not default for privileged app
+     * isn't granted. Example: Location permission for Camera app
+     * @param packageName
+     * @param notPermittedGroups
+     */
+    public void verifyNotPermittedDangerousPermissionDenied(String packageName,
+            String[] notPermittedGroups) {
+        List<String> allNotPermittedDangerousPermsList = getAllDangerousPermissionsByPermGrpNames(
+                notPermittedGroups);
+        List<String> allPermissionsForPackageList = getPermissionByPackage(packageName,
+                Boolean.TRUE);
+        int allNotPermittedDangerousPermsCount = allNotPermittedDangerousPermsList.size();
+        allNotPermittedDangerousPermsList.removeAll(allPermissionsForPackageList);
+        Assert.assertTrue(
+                String.format("For package %s not permissible dangerous permissions been granted",
+                        packageName),
+                allNotPermittedDangerousPermsList.size() == allNotPermittedDangerousPermsCount);
+    }
+
+    /**
+     * Verify any normal permission mentioned in manifest is auto granted
+     * @param packageName
+     */
+    public void verifyNormalPermissionsAutoGranted(String packageName) {
+        List<String> allDeniedPermissionsForPackageList = getPermissionByPackage(packageName,
+                Boolean.FALSE);
+        List<String> allPlatformDangerousPermissionList = getPlatformDangerousPermissionGroupNames();
+        allDeniedPermissionsForPackageList.removeAll(allPlatformDangerousPermissionList);
+        if (!allDeniedPermissionsForPackageList.isEmpty()) {
+            for (int i = 0; i < allDeniedPermissionsForPackageList.size(); ++i) {
+                Log.d(TEST_TAG, String.format("%s should have been auto granted",
+                        allDeniedPermissionsForPackageList.get(i)));
+            }
+        }
+        Assert.assertTrue(
+                String.format("For package %s few normal permission have been denied", packageName),
+                allDeniedPermissionsForPackageList.isEmpty());
+    }
+
+    /**
+     * Verifies via UI that a permission is set/unset for an app
+     * @param appName
+     * @param permission
+     * @param expected : 'ON' or 'OFF'
+     * @return
+     */
+    public Boolean verifyPermissionSettingStatus(String appName, String permission,
+            PermissionStatus expected) {
+        if (!expected.equals(PermissionStatus.ON) && !expected.equals(PermissionStatus.OFF)) {
+            throw new RuntimeException(String.format("%s isn't valid permission status", expected));
+        }
+        openAppPermissionView(appName);
+        UiObject2 permissionView = mDevice
+                .wait(Until.findObject(By.res("android:id/list_container")), TIMEOUT);
+        List<UiObject2> permissionsList = permissionView.getChildren().get(0).getChildren();
+        for (UiObject2 permDesc : permissionsList) {
+            if (permDesc.getChildren().get(1).getChildren().get(0).getText().equals(permission)) {
+                String status = permDesc.getChildren().get(2).getChildren().get(0).getText();
+                return status.equals(expected.toString().toUpperCase());
+            }
+        }
+        Assert.fail("Permission is not found");
+        return Boolean.FALSE;
+    }
+
+    /**
+     * Verify default dangerous permission mentioned in manifest for system privileged apps are auto
+     * permitted Example: Camera permission for Camera app
+     * @param packageName
+     * @param permittedGroups
+     */
+    public void verifyDefaultDangerousPermissionGranted(String packageName,
+            String[] permittedGroups,
+            Boolean byGroup) {
+        List<String> allPermittedDangerousPermsList = new ArrayList<String>();
+        if (byGroup) {
+            allPermittedDangerousPermsList = getAllDangerousPermissionsByPermGrpNames(
+                    permittedGroups);
+        } else {
+            allPermittedDangerousPermsList.addAll(Arrays.asList(permittedGroups));
+        }
+
+        List<String> allPermissionsForPackageList = getPermissionByPackage(packageName,
+                Boolean.TRUE);
+        allPermittedDangerousPermsList.removeAll(allPermissionsForPackageList);
+        for (String permission : allPermittedDangerousPermsList) {
+            Log.d(TEST_TAG,
+                    String.format("%s - > %s hasn't been granted yet", packageName, permission));
+        }
+        Assert.assertTrue(String.format("For %s some Permissions aren't granted yet", packageName),
+                allPermittedDangerousPermsList.isEmpty());
+    }
+
+    /**
+     * For a given app, opens the permission settings window settings -> apps -> permissions
+     * @param appName
+     */
+    public void openAppPermissionView(String appName) {
+        mDevice.pressHome();
+        launchApp(SETTINGS_PACKAGE, "Settings");
+        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)) {
+            view = mDevice.wait(Until.findObject(By.res(SETTINGS_PACKAGE, "main_content")),
+                    TIMEOUT);
+            // todo scroll may be different for device and build
+            view.scroll(Direction.DOWN, 1.0f);
+        }
+
+        mDevice.wait(Until.findObject(By.res("android:id/title").text("Apps")),
+                TIMEOUT)
+                .clickAndWait(Until.newWindow(), TIMEOUT);
+        app = null;
+        view = null;
+        maxAttempt = 10;
+        while ((maxAttempt-- > 0)
+                && ((app = mDevice.wait(Until.findObject(By.res("android:id/title").text(appName)),
+                        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
+            view.scroll(Direction.DOWN, 1.0f);
+        }
+        app.clickAndWait(Until.newWindow(), TIMEOUT);
+        mDevice.wait(Until.findObject(By.res("android:id/title").text("Permissions")),
+                TIMEOUT).clickAndWait(Until.newWindow(), TIMEOUT);
+    }
+
+    /**
+     * Toggles permission for an app via UI
+     * @param appName
+     * @param permission
+     * @param toBeSet
+     */
+    public void togglePermissionSetting(String appName, String permission, Boolean toBeSet) {
+        openAppPermissionView(appName);
+        UiObject2 permissionView = mDevice
+                .wait(Until.findObject(By.res("android:id/list_container")), TIMEOUT);
+        List<UiObject2> permissionsList = permissionView.getChildren().get(0).getChildren();
+        for (UiObject2 obj : permissionsList) {
+            if (obj.getChildren().get(1).getChildren().get(0).getText().equals(permission)) {
+                String status = obj.getChildren().get(2).getChildren().get(0).getText();
+                if ((toBeSet && !status.equals(PermissionStatus.ON.toString()))
+                        || (!toBeSet && status.equals(PermissionStatus.ON.toString()))) {
+                    obj.getChildren().get(2).getChildren().get(0).click();
+                    mDevice.waitForIdle();
+                }
+                break;
+            }
+        }
+    }
+
+    /**
+     * Grant or revoke permission via adb command
+     * @param packageName
+     * @param permissionName
+     * @param permissionOp : Accepted values are 'grant' and 'revoke'
+     */
+    public void grantOrRevokePermissionViaAdb(String packageName, String permissionName,
+            PermissionOp permissionOp) {
+        if (permissionOp == null) {
+            throw new RuntimeException("null operation can't be executed");
+        }
+        String command = String.format("pm %s %s %s", permissionOp.toString().toLowerCase(),
+                packageName, permissionName);
+        Log.d(TEST_TAG, String.format("executing - %s", command));
+        mUiAutomation.executeShellCommand(command);
+        mDevice.waitForIdle();
+    }
+
+    /**
+     * returns list of specific permissions in a dangerous permission group
+     * @param permissionGroupsToCheck
+     * @return
+     */
+    public List<String> getAllDangerousPermissionsByPermGrpNames(String[] permissionGroupsToCheck) {
+        List<String> allDangerousPermissions = new ArrayList<String>();
+        for (String s : permissionGroupsToCheck) {
+            String grpName = String.format("android.permission-group.%s", s.toUpperCase());
+            if (PermissionHelper.mPermissionGroupInfo.keySet().contains(grpName)) {
+                allDangerousPermissions.addAll(PermissionHelper.mPermissionGroupInfo.get(grpName));
+            }
+        }
+
+        return allDangerousPermissions;
+    }
+
+    /**
+     * Returns platform dangerous permission group names
+     * @return
+     */
+    public List<String> getPlatformDangerousPermissionGroupNames() {
+        List<String> allDangerousPermissions = new ArrayList<String>();
+        for (List<String> prmsList : PermissionHelper.mPermissionGroupInfo.values()) {
+            allDangerousPermissions.addAll(prmsList);
+        }
+        return allDangerousPermissions;
+    }
+
+    /**
+     * To ensure that all default dangerous permissions mentioned in manifest are granted for any
+     * privileged app
+     * @param packageName
+     * @param granted
+     * @param denied
+     */
+    public void ensureAppHasDefaultPermissions(String packageName, String[] granted,
+            String[] denied) {
+        List<String> defaultGranted = getAllDangerousPermissionsByPermGrpNames(granted);
+        List<String> currentGranted = getPermissionByPackage(packageName, Boolean.TRUE);
+        List<String> defaultDenied = getAllDangerousPermissionsByPermGrpNames(denied);
+        List<String> currentDenied = getPermissionByPackage(packageName, Boolean.FALSE);
+        defaultGranted.removeAll(currentGranted);
+        for (String permission : defaultGranted) {
+            grantOrRevokePermissionViaAdb(packageName, permission, PermissionOp.GRANT);
+        }
+        defaultDenied.removeAll(currentDenied);
+        for (String permission : defaultDenied) {
+            grantOrRevokePermissionViaAdb(packageName, permission, PermissionOp.REVOKE);
+        }
+    }
+
+    /**
+     * Get permission description via UI
+     * @param appName
+     * @return
+     */
+    public List<String> getPermissionDescGroupNames(String appName) {
+        List<String> groupNames = new ArrayList<String>();
+        openAppPermissionView(appName);
+        mDevice.wait(Until.findObject(By.desc("More options")), TIMEOUT).click();
+        mDevice.wait(Until.findObject(By.text("All permissions")), TIMEOUT).click();
+        UiObject2 permissionsListView = mDevice
+                .wait(Until.findObject(By.res("android:id/list_container")), TIMEOUT);
+        List<UiObject2> permissionList = permissionsListView
+                .findObjects(By.clazz("android.widget.TextView"));
+        for (UiObject2 obj : permissionList) {
+            if (obj.getText() != null && obj.getText() != "" && obj.getVisibleBounds().left == 0) {
+                if (obj.getText().equals("Other app capabilities"))
+                    break;
+                groupNames.add(obj.getText().toUpperCase());
+            }
+        }
+        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/tests/functional/settingstests/Android.mk b/tests/functional/settingstests/Android.mk
new file mode 100644
index 0000000..4b373f5
--- /dev/null
+++ b/tests/functional/settingstests/Android.mk
@@ -0,0 +1,31 @@
+# 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.
+
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_PACKAGE_NAME := SettingsFunctionalTests
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_MODULE_TAGS := tests
+LOCAL_STATIC_JAVA_LIBRARIES := \
+	base-app-helpers \
+	launcher-helper-lib \
+	services.core \
+	settings-app-helper \
+	timeresult-helper-lib \
+	ub-uiautomator
+
+#LOCAL_SDK_VERSION := current
+
+include $(BUILD_PACKAGE)
diff --git a/tests/functional/settingstests/AndroidManifest.xml b/tests/functional/settingstests/AndroidManifest.xml
new file mode 100644
index 0000000..cbd1bc1
--- /dev/null
+++ b/tests/functional/settingstests/AndroidManifest.xml
@@ -0,0 +1,36 @@
+<?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.settings.functional">
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <uses-sdk android:minSdkVersion="19"
+          android:targetSdkVersion="24"/>
+
+    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
+    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+    <uses-permission android:name="android.permission.BLUETOOTH" />
+    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
+
+    <instrumentation
+            android:name="android.test.InstrumentationTestRunner"
+            android:targetPackage="android.settings.functional"
+            android:label="Android Settings Functional Tests" />
+</manifest>
diff --git a/tests/functional/settingstests/src/com/android/settings/functional/AboutPhoneSettingsTests.java b/tests/functional/settingstests/src/com/android/settings/functional/AboutPhoneSettingsTests.java
new file mode 100644
index 0000000..2d6171b
--- /dev/null
+++ b/tests/functional/settingstests/src/com/android/settings/functional/AboutPhoneSettingsTests.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.settings.functional;
+
+import android.content.Intent;
+import android.os.RemoteException;
+import android.provider.Settings;
+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.MediumTest;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+
+/** Verifies basic functionality of the About Phone screen */
+public class AboutPhoneSettingsTests extends InstrumentationTestCase {
+    private static final boolean LOCAL_LOGV = false;
+    private static final String SETTINGS_PACKAGE = "com.android.settings";
+    private static final String TAG = "AboutPhoneSettingsTest";
+    private static final int TIMEOUT = 2000;
+
+    private UiDevice mDevice;
+
+    // TODO: retrieve using name/ids from com.android.settings package
+    private static final String[] sResourceTexts = {
+        "Status",
+        "Legal information",
+        "Regulatory information",
+        "Model number",
+        "Android version",
+        "Android security patch level",
+        "Baseband version",
+        "Kernel version",
+        "Build number"
+    };
+
+    private static final String[] sClickableResourceTexts = {
+        "Status", "Legal information", "Regulatory information",
+    };
+
+    @Override
+    public void setUp() throws Exception {
+        if (LOCAL_LOGV) {
+            Log.d(TAG, "-------");
+        }
+        super.setUp();
+        mDevice = UiDevice.getInstance(getInstrumentation());
+        try {
+            mDevice.setOrientationNatural();
+        } catch (RemoteException e) {
+            throw new RuntimeException("Failed to freeze device orientaion", e);
+        }
+
+        // make sure we are in a clean state before starting the test
+        mDevice.pressHome();
+        Thread.sleep(TIMEOUT * 2);
+        launchAboutPhoneSettings(Settings.ACTION_DEVICE_INFO_SETTINGS);
+        // TODO: make sure we are always at the top of the app
+        // currently this will fail if the user has navigated into submenus
+        UiObject2 view =
+                mDevice.wait(
+                        Until.findObject(By.res(SETTINGS_PACKAGE + ":id/main_content")), TIMEOUT);
+        assertNotNull("Could not find main About Phone screen", view);
+        view.scroll(Direction.UP, 1.0f);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        mDevice.pressHome(); // finish settings activity
+        mDevice.waitForIdle(TIMEOUT * 2); // give UI time to finish animating
+        super.tearDown();
+    }
+
+    private void launchAboutPhoneSettings(String aboutSetting) throws Exception {
+        Intent aboutIntent = new Intent(aboutSetting);
+        aboutIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        getInstrumentation().getContext().startActivity(aboutIntent);
+    }
+
+    /**
+     * Callable actions that can be taken when a UIObject2 is found
+     *
+     * @param device The current UiDevice
+     * @param item The UiObject2 that was found and can be acted on
+     *
+     * @return {@code true} if the call was successful, and {@code false} otherwise
+     */
+    public interface UIObject2Callback {
+        boolean call(UiDevice device, UiObject2 item) throws Exception;
+    }
+
+    /**
+     * Clicks the given item and then presses the Back button
+     *
+     * <p>Used to test whether a given UiObject2 can be successfully clicked.
+     * Presses Back to restore state to the previous screen.
+     *
+     * @param device The device that can be used to press Back
+     * @param item The item to click
+     *
+     * @return {@code true} if clicking the item succeeded, and {@code false} otherwise
+     */
+    public class UiObject2Clicker implements UIObject2Callback {
+        public boolean call(UiDevice device, UiObject2 item) throws Exception {
+            item.click();
+            Thread.sleep(TIMEOUT * 2); // give UI time to finish animating
+            boolean pressWorked = device.pressBack();
+            Thread.sleep(TIMEOUT * 2);
+            return pressWorked;
+        }
+    }
+
+    /**
+     * Removes items found in the view and optionally takes some action.
+     *
+     * @param device The current UiDevice
+     * @param itemsLeftToFind The items to search for in the current view
+     * @param action Action to call on each item that is found; pass {@code null} to take no action
+     */
+    private void removeItemsAndTakeAction(
+            UiDevice device, ArrayList<String> itemsLeftToFind, UIObject2Callback action) throws Exception {
+        for (Iterator<String> iterator = itemsLeftToFind.iterator(); iterator.hasNext(); ) {
+            String itemText = iterator.next();
+            UiObject2 item = device.wait(Until.findObject(By.text(itemText)), TIMEOUT);
+            if (item != null) {
+                if (LOCAL_LOGV) {
+                    Log.d(TAG, itemText + " is present");
+                }
+                iterator.remove();
+                if (action != null) {
+                    boolean success = action.call(device, item);
+                    assertTrue("Calling action after " + itemText + " did not work", success);
+                }
+            } else {
+                if (LOCAL_LOGV) {
+                    Log.d(TAG, "Could not find " + itemText);
+                }
+            }
+        }
+    }
+
+    /**
+     * Searches for UI elements in the current view and optionally takes some action.
+     *
+     * <p>Will scroll down the screen until it has found all elements or reached the bottom.
+     * This allows elements to be found and acted on even if they change order.
+     *
+     * @param device The current UiDevice
+     * @param itemsToFind The items to search for in the current view
+     * @param action Action to call on each item that is found; pass {@code null} to take no action
+     */
+    public void searchForItemsAndTakeAction(UiDevice device, String[] itemsToFind, UIObject2Callback action)
+            throws Exception {
+
+        ArrayList<String> itemsLeftToFind = new ArrayList<String>(Arrays.asList(itemsToFind));
+        assertFalse(
+                "There must be at least one item to search for on the screen!",
+                itemsLeftToFind.isEmpty());
+
+        if (LOCAL_LOGV) {
+            Log.d(TAG, "items: " + TextUtils.join(", ", itemsLeftToFind));
+        }
+        boolean canScrollDown = true;
+        while (canScrollDown && !itemsLeftToFind.isEmpty()) {
+            removeItemsAndTakeAction(device, itemsLeftToFind, action);
+
+            // when we've finished searching the current view, scroll down
+            UiObject2 view =
+                    device.wait(
+                            Until.findObject(By.res(SETTINGS_PACKAGE + ":id/main_content")),
+                            TIMEOUT * 2);
+            if (view != null) {
+                canScrollDown = view.scroll(Direction.DOWN, 1.0f);
+            } else {
+                canScrollDown = false;
+            }
+        }
+        // check the last items once we have reached the bottom of the view
+        removeItemsAndTakeAction(device, itemsLeftToFind, action);
+
+        assertTrue(
+                "The following items were not found on the screen: "
+                        + TextUtils.join(", ", itemsLeftToFind),
+                itemsLeftToFind.isEmpty());
+    }
+
+    @MediumTest // UI interaction
+    public void testAllMenuEntriesExist() throws Exception {
+        searchForItemsAndTakeAction(mDevice, sResourceTexts, null);
+    }
+
+    @MediumTest // UI interaction
+    public void testClickableEntriesCanBeClicked() throws Exception {
+        searchForItemsAndTakeAction(mDevice, sClickableResourceTexts, new UiObject2Clicker());
+    }
+}
diff --git a/tests/functional/settingstests/src/com/android/settings/functional/AccessibilitySettingsTests.java b/tests/functional/settingstests/src/com/android/settings/functional/AccessibilitySettingsTests.java
new file mode 100644
index 0000000..c3a59da
--- /dev/null
+++ b/tests/functional/settingstests/src/com/android/settings/functional/AccessibilitySettingsTests.java
@@ -0,0 +1,247 @@
+/*
+ * 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.settings.functional;
+
+import android.content.Context;
+import android.net.wifi.WifiManager;
+import android.os.RemoteException;
+import android.platform.test.helpers.SettingsHelperImpl;
+import android.provider.Settings;
+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.MediumTest;
+import android.test.suitebuilder.annotation.Suppress;
+
+public class AccessibilitySettingsTests extends InstrumentationTestCase {
+
+    private static final String SETTINGS_PACKAGE = "com.android.settings";
+    private static final int TIMEOUT = 2000;
+    private UiDevice mDevice;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mDevice = UiDevice.getInstance(getInstrumentation());
+        try {
+            mDevice.setOrientationNatural();
+        } catch (RemoteException e) {
+            throw new RuntimeException("failed to freeze device orientaion", e);
+        }
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        // Need to finish settings activity
+        mDevice.pressBack();
+        mDevice.pressHome();
+        mDevice.waitForIdle();
+        super.tearDown();
+    }
+
+    @MediumTest
+    public void testHighContrastTextOn() throws Exception {
+        verifyAccessibilitySettingOnOrOff("High contrast text",
+                Settings.Secure.ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED, 0, 1);
+    }
+
+    @MediumTest
+    public void testHighContrastTextOff() throws Exception {
+        verifyAccessibilitySettingOnOrOff("High contrast text",
+               Settings.Secure.ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED, 1, 0);
+    }
+
+    @MediumTest
+    public void testPowerButtonEndsCallOn() throws Exception {
+        verifyAccessibilitySettingOnOrOff("Power button ends call",
+                Settings.Secure.INCALL_POWER_BUTTON_BEHAVIOR, 1, 2);
+    }
+
+    @MediumTest
+    public void testPowerButtonEndsCallOff() throws Exception {
+        verifyAccessibilitySettingOnOrOff("Power button ends call",
+                Settings.Secure.INCALL_POWER_BUTTON_BEHAVIOR, 2, 1);
+    }
+
+    /* Suppressing these four tests. The settings don't play
+     * nice with Settings.System.putInt or Settings.Secure.putInt.
+     * Need further clarification. Filed bug b/27792029
+     */
+    @Suppress
+    @MediumTest
+    public void testAutoRotateScreenOn() throws Exception {
+        verifyAccessibilitySettingOnOrOff("Auto-rotate screen",
+               Settings.System.ACCELEROMETER_ROTATION, 0, 1);
+    }
+
+    @Suppress
+    @MediumTest
+    public void testAutoRotateScreenOff() throws Exception {
+       verifyAccessibilitySettingOnOrOff("Auto-rotate screen",
+               Settings.System.ACCELEROMETER_ROTATION, 1, 0);
+    }
+
+    @Suppress
+    @MediumTest
+    public void testMonoAudioOn() throws Exception {
+        verifyAccessibilitySettingOnOrOff("Mono audio",
+               Settings.System.MASTER_MONO, 0, 1);
+    }
+
+    @Suppress
+    @MediumTest
+    public void testMonoAudioOff() throws Exception {
+         verifyAccessibilitySettingOnOrOff("Mono audio",
+                Settings.System.MASTER_MONO, 1, 0);
+    }
+
+    @MediumTest
+    public void testSpeakPasswordsOn() throws Exception {
+        verifyAccessibilitySettingOnOrOff("Speak passwords",
+                Settings.Secure.ACCESSIBILITY_SPEAK_PASSWORD, 0, 1);
+    }
+
+    @MediumTest
+    public void testSpeakPasswordsOff() throws Exception {
+        verifyAccessibilitySettingOnOrOff("Speak passwords",
+                 Settings.Secure.ACCESSIBILITY_SPEAK_PASSWORD, 1, 0);
+     }
+
+    @MediumTest
+    public void testLargeMousePointerOn() throws Exception {
+         verifyAccessibilitySettingOnOrOff("Large mouse pointer",
+                 Settings.Secure.ACCESSIBILITY_LARGE_POINTER_ICON, 0, 1);
+    }
+
+    @MediumTest
+    public void testLargeMousePointerOff() throws Exception {
+         verifyAccessibilitySettingOnOrOff("Large mouse pointer",
+                 Settings.Secure.ACCESSIBILITY_LARGE_POINTER_ICON, 1, 0);
+    }
+
+    @MediumTest
+    public void testColorCorrection() throws Exception {
+        verifySettingToggleAfterScreenLoad("Color correction",
+                Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED);
+    }
+
+    // Suppressing this test, since UiAutomator + talkback don't play nice
+    @Suppress
+    @MediumTest
+    public void testTalkback() throws Exception {
+        verifySettingToggleAfterScreenLoad("TalkBack",
+                Settings.Secure.ACCESSIBILITY_ENABLED);
+    }
+
+    @MediumTest
+    public void testCaptions() throws Exception {
+         verifySettingToggleAfterScreenLoad("Captions",
+                 Settings.Secure.ACCESSIBILITY_CAPTIONING_ENABLED);
+    }
+
+    @MediumTest
+    public void testMagnificationGesture() throws Exception {
+         verifySettingToggleAfterScreenLoad("Magnification gesture",
+                 Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED);
+    }
+
+    @MediumTest
+    public void testClickAfterPointerStopsMoving() throws Exception {
+         verifySettingToggleAfterScreenLoad("Click after pointer stops moving",
+                  Settings.Secure.ACCESSIBILITY_AUTOCLICK_ENABLED);
+    }
+
+    public void launchAccessibilitySettings() throws Exception {
+        SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(),
+                Settings.ACTION_ACCESSIBILITY_SETTINGS);
+    }
+
+    private void verifyAccessibilitySettingOnOrOff(String settingText,
+            String settingFlag, int initialFlagValue, int expectedFlagValue) throws Exception {
+        Settings.Secure.putInt(getInstrumentation().getContext().getContentResolver(),
+                settingFlag, initialFlagValue);
+        launchAccessibilitySettings();
+        UiObject2 settingsTitle = findItemOnScreen(settingText);
+        settingsTitle.click();
+        Thread.sleep(TIMEOUT);
+        int settingValue = Settings.Secure
+                .getInt(getInstrumentation().getContext().getContentResolver(), settingFlag);
+        assertEquals(settingText + " not correctly set after toggle", expectedFlagValue, settingValue);
+    }
+
+    private void verifySettingToggleAfterScreenLoad(String settingText, String settingFlag) throws Exception {
+        // Load accessibility settings
+        launchAccessibilitySettings();
+        Settings.Secure.putInt(getInstrumentation().getContext().getContentResolver(),
+                settingFlag, 0);
+        Thread.sleep(TIMEOUT);
+        // Tap on setting required
+        UiObject2 settingTitle = findItemOnScreen(settingText);
+        // Load screen
+        settingTitle.click();
+        Thread.sleep(TIMEOUT);
+        // Toggle value
+        UiObject2 settingToggle =  mDevice.wait(Until.findObject(By.text("Off")),
+                            TIMEOUT);
+        settingToggle.click();
+        dismissOpenDialog();
+        Thread.sleep(TIMEOUT);
+        // Assert new value
+        int settingValue = Settings.Secure.
+                getInt(getInstrumentation().getContext().getContentResolver(), settingFlag);
+        assertEquals(settingText + " value not set correctly", 1, settingValue);
+        // Toogle value
+        settingToggle.click();
+        dismissOpenDialog();
+        mDevice.pressBack();
+        Thread.sleep(TIMEOUT);
+        // Assert reset to old value
+        settingValue = Settings.Secure.
+                getInt(getInstrumentation().getContext().getContentResolver(), settingFlag);
+        assertEquals(settingText + " value not set correctly", 0, settingValue);
+    }
+
+    private UiObject2 findItemOnScreen(String item) throws Exception {
+        int count = 0;
+        UiObject2 settingsPanel = mDevice.wait(Until.findObject
+                (By.res(SETTINGS_PACKAGE, "list")), TIMEOUT);
+        while (settingsPanel.fling(Direction.UP) && count < 3) {
+            count++;
+        }
+        count = 0;
+        UiObject2 setting = null;
+        while(count < 3 && setting == null) {
+            setting = mDevice.wait(Until.findObject(By.text(item)), TIMEOUT);
+            if (setting == null) {
+                settingsPanel.scroll(Direction.DOWN, 1.0f);
+            }
+            count++;
+        }
+        return setting;
+    }
+
+    private void dismissOpenDialog() throws Exception {
+        UiObject2 okButton = mDevice.wait(Until.findObject
+                (By.res("android:id/button1")), TIMEOUT*2);
+        if (okButton != null) {
+            okButton.click();
+        }
+    }
+}
diff --git a/tests/functional/settingstests/src/com/android/settings/functional/BluetoothNetworkSettingsTests.java b/tests/functional/settingstests/src/com/android/settings/functional/BluetoothNetworkSettingsTests.java
new file mode 100644
index 0000000..a55fea6
--- /dev/null
+++ b/tests/functional/settingstests/src/com/android/settings/functional/BluetoothNetworkSettingsTests.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.settings.functional;
+
+import java.io.IOException;
+import android.content.Context;
+import android.content.Intent;
+import android.bluetooth.BluetoothManager;
+import android.bluetooth.BluetoothAdapter;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.provider.Settings;
+import android.provider.Settings.SettingNotFoundException;
+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.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+
+
+public class BluetoothNetworkSettingsTests extends InstrumentationTestCase {
+
+    private static final String SETTINGS_PACKAGE = "com.android.settings";
+    private static final int TIMEOUT = 2000;
+    private UiDevice mDevice;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mDevice = UiDevice.getInstance(getInstrumentation());
+        try {
+            mDevice.setOrientationNatural();
+        } catch (RemoteException e) {
+            throw new RuntimeException("failed to freeze device orientaion", e);
+        }
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        mDevice.pressBack();
+        mDevice.pressHome();
+        mDevice.waitForIdle();
+        super.tearDown();
+    }
+
+    @MediumTest
+    public void testBluetoothEnabled() throws Exception {
+        verifyBluetoothOnOrOff(true);
+    }
+
+    @MediumTest
+    public void testBluetoothDisabled() throws Exception {
+        verifyBluetoothOnOrOff(false);
+    }
+
+    @MediumTest
+    public void testRefreshOverflowOption() throws Exception {
+        verifyBluetoothOverflowOptions("Refresh", false, null);
+    }
+
+    @MediumTest
+    public void testRenameOverflowOption() throws Exception {
+        verifyBluetoothOverflowOptions("Rename this device", true, "RENAME");
+    }
+
+    @MediumTest
+    public void testReceivedFilesOverflowOption() throws Exception {
+        verifyBluetoothOverflowOptions("Show received files", true, "Bluetooth received");
+    }
+
+    @MediumTest
+    public void testHelpFeedbackOverflowOption() throws Exception {
+        verifyBluetoothOverflowOptions("Help & feedback", true, "Help");
+    }
+
+    public void launchBluetoothSettings() throws Exception {
+        Intent btIntent = new Intent(Settings.ACTION_BLUETOOTH_SETTINGS);
+        btIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        getInstrumentation().getContext().startActivity(btIntent);
+        Thread.sleep(TIMEOUT * 2);
+    }
+
+    /**
+     * Verifies clicking on the BT overflow option and loading the right screen
+     * @param overflowOptionText the text of the option to be clicked
+     * @param verifyClick if you need a click to be verified
+     * @param optionLoaded text of an element on the post click screen for verification
+     */
+    public void verifyBluetoothOverflowOptions(String overflowOptionText, boolean verifyClick,
+            String optionLoaded) throws Exception {
+        BluetoothAdapter bluetoothAdapter = ((BluetoothManager) getInstrumentation().getContext()
+                .getSystemService(Context.BLUETOOTH_SERVICE)).getAdapter();
+        bluetoothAdapter.enable();
+        launchBluetoothSettings();
+        mDevice.wait(Until.findObject(By.desc("More options")), TIMEOUT).click();
+        Thread.sleep(TIMEOUT);
+        UiObject2 overflowOption = mDevice.wait(Until.findObject(By.text(overflowOptionText)),
+                TIMEOUT);
+        assertNotNull(overflowOptionText + " option is not present in advanced Bluetooth menu",
+                overflowOption);
+        if (verifyClick) {
+            overflowOption.click();
+            // Adding an extra back press to deal with IME+UiAutomator bug
+            if (optionLoaded.equals("RENAME")) {
+                mDevice.pressBack();
+            }
+            UiObject2 loadOption = mDevice.wait(Until.findObject(By.text(optionLoaded)), TIMEOUT);
+            assertNotNull(overflowOptionText + " option did not load correctly on tapping",
+                    loadOption);
+        }
+    }
+
+    /**
+     * Toggles the Bluetooth switch and verifies that the change is reflected in Settings
+     * @param verifyOn set to whether you want the setting turned On or Off
+     */
+    private void verifyBluetoothOnOrOff(boolean verifyOn) throws Exception {
+        String switchText = "ON";
+        BluetoothAdapter bluetoothAdapter = ((BluetoothManager) getInstrumentation().getContext()
+                            .getSystemService(Context.BLUETOOTH_SERVICE)).getAdapter();
+        if (verifyOn) {
+            switchText = "OFF";
+            bluetoothAdapter.disable();
+         }
+         else {
+             bluetoothAdapter.enable();
+         }
+         launchBluetoothSettings();
+         mDevice.wait(Until
+                 .findObject(By.res(SETTINGS_PACKAGE, "switch_widget").text(switchText)), TIMEOUT)
+                 .click();
+         Thread.sleep(TIMEOUT);
+         String bluetoothValue =
+                 Settings.Global.getString(getInstrumentation().getContext().getContentResolver(),
+                 Settings.Global.BLUETOOTH_ON);
+         if (verifyOn) {
+             assertEquals("1", bluetoothValue);
+         }
+         else {
+             assertEquals("0", bluetoothValue);
+         }
+    }
+}
diff --git a/tests/functional/settingstests/src/com/android/settings/functional/DataUsageSettingsTests.java b/tests/functional/settingstests/src/com/android/settings/functional/DataUsageSettingsTests.java
new file mode 100644
index 0000000..202264b
--- /dev/null
+++ b/tests/functional/settingstests/src/com/android/settings/functional/DataUsageSettingsTests.java
@@ -0,0 +1,84 @@
+/*
+ * 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.settings.functional;
+
+import android.content.Context;
+import android.net.wifi.WifiManager;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.platform.test.helpers.SettingsHelperImpl;
+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.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+
+public class DataUsageSettingsTests extends InstrumentationTestCase {
+
+    private static final String SETTINGS_PACKAGE = "com.android.settings";
+    private static final int TIMEOUT = 2000;
+    private UiDevice mDevice;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mDevice = UiDevice.getInstance(getInstrumentation());
+        try {
+            mDevice.setOrientationNatural();
+        } catch (RemoteException e) {
+            throw new RuntimeException("failed to freeze device orientaion", e);
+        }
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        // Need to finish settings activity
+        mDevice.pressBack();
+        mDevice.pressHome();
+        super.tearDown();
+    }
+
+    @MediumTest
+    public void testElementsOnDataUsageScreen() throws Exception {
+        launchDataUsageSettings();
+        assertNotNull("Data usage element not found",
+                mDevice.wait(Until.findObject(By.text("Usage")),
+                TIMEOUT));
+        assertNotNull("Data usage bar not found",
+                mDevice.wait(Until.findObject(By.res(SETTINGS_PACKAGE,
+                "color_bar")), TIMEOUT));
+        assertNotNull("Data saver element not found",
+                mDevice.wait(Until.findObject(By.text("Data saver")),
+                TIMEOUT));
+        assertNotNull("WiFi Data usage element not found",
+                mDevice.wait(Until.findObject(By.text("Wi-Fi data usage")),
+                TIMEOUT));
+        assertNotNull("Network restrictions element not found",
+                mDevice.wait(Until.findObject(By.text("Network restrictions")),
+                TIMEOUT));
+    }
+
+    public void launchDataUsageSettings() throws Exception {
+        SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(),
+                Settings.ACTION_SETTINGS);
+        mDevice.wait(Until
+                .findObject(By.text("Data usage")), TIMEOUT)
+                .click();
+        Thread.sleep(TIMEOUT * 2);
+    }
+}
diff --git a/tests/functional/settingstests/src/com/android/settings/functional/DisplaySettingsTest.java b/tests/functional/settingstests/src/com/android/settings/functional/DisplaySettingsTest.java
new file mode 100644
index 0000000..54653c4
--- /dev/null
+++ b/tests/functional/settingstests/src/com/android/settings/functional/DisplaySettingsTest.java
@@ -0,0 +1,267 @@
+/*
+ * 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.settings.functional;
+
+import android.content.ContentResolver;
+import android.provider.Settings;
+import android.platform.test.helpers.SettingsHelperImpl;
+import android.platform.test.helpers.SettingsHelperImpl.SettingsType;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.Until;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.test.suitebuilder.annotation.Suppress;
+
+import java.util.regex.Pattern;
+
+public class DisplaySettingsTest extends InstrumentationTestCase {
+
+    private static final String PAGE = Settings.ACTION_DISPLAY_SETTINGS;
+    private static final int TIMEOUT = 2000;
+    private static final FontSetting FONT_SMALL = new FontSetting("Small", 0.85f);
+    private static final FontSetting FONT_NORMAL = new FontSetting("Default", 1.00f);
+    private static final FontSetting FONT_LARGE = new FontSetting("Large", 1.15f);
+    private static final FontSetting FONT_HUGE = new FontSetting("Largest", 1.30f);
+
+    private UiDevice mDevice;
+    private ContentResolver mResolver;
+    private SettingsHelperImpl mHelper;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mDevice = UiDevice.getInstance(getInstrumentation());
+        mDevice.setOrientationNatural();
+        mResolver = getInstrumentation().getContext().getContentResolver();
+        mHelper = new SettingsHelperImpl(getInstrumentation());
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        // reset settings we touched that may impact others
+        Settings.System.putFloat(mResolver, Settings.System.FONT_SCALE, 1.00f);
+        mDevice.waitForIdle();
+        super.tearDown();
+    }
+
+    @MediumTest
+    public void testAdaptiveBrightness() throws Exception {
+        SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+        mHelper.scrollVert(true);
+        Thread.sleep(1000);
+        assertTrue(mHelper.verifyToggleSetting(SettingsType.SYSTEM, PAGE, "Adaptive brightness",
+                Settings.System.SCREEN_BRIGHTNESS_MODE));
+        assertTrue(mHelper.verifyToggleSetting(SettingsType.SYSTEM, PAGE, "Adaptive brightness",
+                Settings.System.SCREEN_BRIGHTNESS_MODE));
+    }
+
+    @MediumTest
+    public void testCameraDoubleTap() throws Exception {
+        assertTrue(mHelper.verifyToggleSetting(SettingsType.SECURE, PAGE,
+                "Press power button twice for camera",
+                Settings.Secure.CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED));
+        assertTrue(mHelper.verifyToggleSetting(SettingsType.SECURE, PAGE,
+                "Press power button twice for camera",
+                Settings.Secure.CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED));
+    }
+
+    @MediumTest
+    public void testAmbientDisplay() throws Exception {
+        // unique to the ambient display setting, null is equivalent to "on",
+        // so we need to populate the setting if it hasn't been yet
+        String initialSetting = Settings.Secure.getString(mResolver, Settings.Secure.DOZE_ENABLED);
+        if (initialSetting == null) {
+            Settings.Secure.putString(mResolver, Settings.Secure.DOZE_ENABLED, "1");
+        }
+        assertTrue(mHelper.verifyToggleSetting(SettingsType.SECURE, PAGE, "Ambient display",
+                Settings.Secure.DOZE_ENABLED));
+        assertTrue(mHelper.verifyToggleSetting(SettingsType.SECURE, PAGE, "Ambient display",
+                Settings.Secure.DOZE_ENABLED));
+    }
+
+    // blocked on b/27487224
+    @MediumTest
+    @Suppress
+    public void testDaydreamToggle() throws Exception {
+        SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+        Pattern p = Pattern.compile("On|Off");
+        mHelper.clickSetting("Screen saver");
+        Thread.sleep(1000);
+        try {
+            assertTrue(mHelper.verifyToggleSetting(SettingsType.SECURE, PAGE, p,
+                    Settings.Secure.SCREENSAVER_ENABLED, false));
+            assertTrue(mHelper.verifyToggleSetting(SettingsType.SECURE, PAGE, p,
+                    Settings.Secure.SCREENSAVER_ENABLED, false));
+        } finally {
+            mDevice.pressBack();
+        }
+    }
+
+    @MediumTest
+    public void testAccelRotation() throws Exception {
+        SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+        mHelper.scrollVert(true);
+        Thread.sleep(4000);
+        String[] buttons = {
+                "Rotate the contents of the screen",
+                "Stay in portrait view"
+        };
+        int currentAccelSetting = Settings.System.getInt(
+                mResolver, Settings.System.ACCELEROMETER_ROTATION);
+        mHelper.scrollVert(false);
+        mHelper.clickSetting("When device is rotated");
+        assertTrue(mHelper.verifyToggleSetting(SettingsType.SYSTEM, PAGE,
+                buttons[currentAccelSetting], Settings.System.ACCELEROMETER_ROTATION, false));
+        mHelper.scrollVert(false);
+        mHelper.clickSetting("When device is rotated");
+        assertTrue(mHelper.verifyToggleSetting(SettingsType.SYSTEM, PAGE,
+                buttons[1 - currentAccelSetting], Settings.System.ACCELEROMETER_ROTATION, false));
+    }
+
+    @MediumTest
+    public void testDaydream() throws Exception {
+        Settings.Secure.putInt(mResolver, Settings.Secure.SCREENSAVER_ENABLED, 1);
+        SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+        try {
+            assertTrue(mHelper.verifyRadioSetting(SettingsType.SECURE, PAGE,
+                    "Screen saver", "Clock", Settings.Secure.SCREENSAVER_COMPONENTS,
+                    "com.google.android.deskclock/com.android.deskclock.Screensaver"));
+            assertTrue(mHelper.verifyRadioSetting(SettingsType.SECURE, PAGE,
+                    null, "Colors", Settings.Secure.SCREENSAVER_COMPONENTS,
+                    "com.android.dreams.basic/com.android.dreams.basic.Colors"));
+            assertTrue(mHelper.verifyRadioSetting(SettingsType.SECURE, PAGE,
+                    null, "Photos", Settings.Secure.SCREENSAVER_COMPONENTS,
+                    "com.google.android.apps.photos/com.google.android.apps.photos.daydream.PhotosDreamService"));
+        } finally {
+            mDevice.pressBack();
+            Thread.sleep(2000);
+        }
+    }
+
+    @MediumTest
+    public void testSleep15Seconds() throws Exception {
+        SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+        assertTrue(mHelper.verifyRadioSetting(SettingsType.SYSTEM, PAGE,
+                "Sleep", "15 seconds", Settings.System.SCREEN_OFF_TIMEOUT, "15000"));
+    }
+
+    @MediumTest
+    public void testSleep30Seconds() throws Exception {
+        SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+        assertTrue(mHelper.verifyRadioSetting(SettingsType.SYSTEM, PAGE,
+                "Sleep", "30 seconds", Settings.System.SCREEN_OFF_TIMEOUT, "30000"));
+    }
+
+    @MediumTest
+    public void testSleep1Minute() throws Exception {
+        SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+        assertTrue(mHelper.verifyRadioSetting(SettingsType.SYSTEM, PAGE,
+                "Sleep", "1 minute", Settings.System.SCREEN_OFF_TIMEOUT, "60000"));
+    }
+
+    @MediumTest
+    public void testSleep2Minutes() throws Exception {
+        SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+        assertTrue(mHelper.verifyRadioSetting(SettingsType.SYSTEM, PAGE,
+                "Sleep", "2 minutes", Settings.System.SCREEN_OFF_TIMEOUT, "120000"));
+    }
+
+    @MediumTest
+    public void testSleep5Minutes() throws Exception {
+        SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+        assertTrue(mHelper.verifyRadioSetting(SettingsType.SYSTEM, PAGE,
+                "Sleep", "5 minutes", Settings.System.SCREEN_OFF_TIMEOUT, "300000"));
+    }
+
+    @MediumTest
+    public void testSleep10Minutes() throws Exception {
+        SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+        assertTrue(mHelper.verifyRadioSetting(SettingsType.SYSTEM, PAGE,
+                "Sleep", "10 minutes", Settings.System.SCREEN_OFF_TIMEOUT, "600000"));
+    }
+
+    @MediumTest
+    public void testSleep30Minutes() throws Exception {
+        SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+        assertTrue(mHelper.verifyRadioSetting(SettingsType.SYSTEM, PAGE,
+                "Sleep", "30 minutes", Settings.System.SCREEN_OFF_TIMEOUT, "1800000"));
+    }
+
+    @MediumTest
+    public void testFontSizeLarge() throws Exception {
+        verifyFontSizeSetting(1.00f, FONT_LARGE);
+        // Leaving the font size at large can make later tests fail, so reset it
+        Settings.System.putFloat(mResolver, Settings.System.FONT_SCALE, 1.00f);
+        // It takes a second for the new font size to be picked up
+        Thread.sleep(2000);
+    }
+
+    @MediumTest
+    public void testFontSizeDefault() throws Exception {
+        verifyFontSizeSetting(1.15f, FONT_NORMAL);
+    }
+
+    @MediumTest
+    public void testFontSizeLargest() throws Exception {
+        verifyFontSizeSetting(1.00f, FONT_HUGE);
+        // Leaving the font size at huge can make later tests fail, so reset it
+        Settings.System.putFloat(mResolver, Settings.System.FONT_SCALE, 1.00f);
+        // It takes a second for the new font size to be picked up
+        Thread.sleep(2000);
+    }
+
+    @MediumTest
+    public void testFontSizeSmall() throws Exception {
+        verifyFontSizeSetting(1.00f, FONT_SMALL);
+    }
+
+    private void verifyFontSizeSetting(float resetValue, FontSetting setting)
+            throws Exception {
+        Settings.System.putFloat(mResolver, Settings.System.FONT_SCALE, resetValue);
+        SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+        mHelper.clickSetting("Font size");
+        try {
+            mDevice.wait(Until.findObject(By.desc(setting.getName())), TIMEOUT).click();
+            Thread.sleep(1000);
+            float changedValue = Settings.System.getFloat(
+                    mResolver, Settings.System.FONT_SCALE);
+            assertEquals(setting.getSize(), changedValue, 0.0001);
+        } finally {
+            // Make sure to back out of the font menu
+            mDevice.pressBack();
+        }
+    }
+
+    private static class FontSetting {
+        private final String mSizeName;
+        private final float mSizeVal;
+
+        public FontSetting(String sizeName, float sizeVal) {
+            mSizeName = sizeName;
+            mSizeVal = sizeVal;
+        }
+
+        public String getName() {
+            return mSizeName;
+        }
+
+        public float getSize() {
+            return mSizeVal;
+        }
+    }
+}
diff --git a/tests/functional/settingstests/src/com/android/settings/functional/LocationSettingsTests.java b/tests/functional/settingstests/src/com/android/settings/functional/LocationSettingsTests.java
new file mode 100644
index 0000000..a6e77bc
--- /dev/null
+++ b/tests/functional/settingstests/src/com/android/settings/functional/LocationSettingsTests.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.settings.functional;
+
+import android.content.Context;
+import android.net.wifi.WifiManager;
+import android.nfc.NfcAdapter;
+import android.nfc.NfcManager;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.platform.test.helpers.SettingsHelperImpl;
+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.MediumTest;
+
+
+public class LocationSettingsTests extends InstrumentationTestCase {
+
+    private static final String SETTINGS_PACKAGE = "com.android.settings";
+    private static final int TIMEOUT = 2000;
+    private UiDevice mDevice;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mDevice = UiDevice.getInstance(getInstrumentation());
+        try {
+            mDevice.setOrientationNatural();
+        } catch (RemoteException e) {
+            throw new RuntimeException("failed to freeze device orientaion", e);
+        }
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        mDevice.pressBack();
+        mDevice.pressHome();
+        super.tearDown();
+    }
+
+    @MediumTest
+    public void testLoadingLocationSettings () throws Exception {
+        // Load settings
+        SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(),
+                Settings.ACTION_SETTINGS);
+        // Tap on location
+        UiObject2 settingsPanel = mDevice.wait(Until.findObject
+                (By.res(SETTINGS_PACKAGE, "dashboard_container")), TIMEOUT);
+        int count = 0;
+        UiObject2 locationTitle = null;
+        while(count < 6 && locationTitle == null) {
+            locationTitle = mDevice.wait(Until.findObject(By.text("Location")), TIMEOUT);
+            if (locationTitle == null) {
+                settingsPanel.scroll(Direction.DOWN, 1.0f);
+            }
+            count++;
+        }
+        // Verify location settings loads.
+        locationTitle.click();
+        Thread.sleep(TIMEOUT);
+        assertNotNull("Location screen has not loaded correctly",
+                mDevice.wait(Until.findObject(By.text("Location services")), TIMEOUT));
+    }
+
+    @MediumTest
+    public void testLocationSettingOn() throws Exception {
+        verifyLocationSettingsOnOrOff(true);
+    }
+
+    @MediumTest
+    public void testLocationSettingOff() throws Exception {
+        verifyLocationSettingsOnOrOff(false);
+    }
+
+    @MediumTest
+    public void testLocationDeviceOnlyMode() throws Exception {
+        // Changing the value from default before testing the toggle to Device only mode
+        Settings.Secure.putInt(getInstrumentation().getContext().getContentResolver(),
+                Settings.Secure.LOCATION_MODE, Settings.Secure.LOCATION_MODE_BATTERY_SAVING);
+        Thread.sleep(TIMEOUT);
+        verifyLocationSettingsMode(Settings.Secure.LOCATION_MODE_SENSORS_ONLY);
+    }
+
+    @MediumTest
+    public void testLocationBatterySavingMode() throws Exception {
+        Settings.Secure.putInt(getInstrumentation().getContext().getContentResolver(),
+                Settings.Secure.LOCATION_MODE, Settings.Secure.LOCATION_MODE_SENSORS_ONLY);
+        Thread.sleep(TIMEOUT);
+        verifyLocationSettingsMode(Settings.Secure.LOCATION_MODE_BATTERY_SAVING);
+    }
+
+    @MediumTest
+    public void testLocationHighAccuracyMode() throws Exception {
+        Settings.Secure.putInt(getInstrumentation().getContext().getContentResolver(),
+                Settings.Secure.LOCATION_MODE, Settings.Secure.LOCATION_MODE_SENSORS_ONLY);
+        Thread.sleep(TIMEOUT);
+        verifyLocationSettingsMode(Settings.Secure.LOCATION_MODE_HIGH_ACCURACY);
+    }
+
+    @MediumTest
+    public void testLocationSettingsElements() throws Exception {
+        String[] textElements = {"Location", "On", "Mode", "Recent location requests",
+                "Location services"};
+        SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(),
+                Settings.ACTION_LOCATION_SOURCE_SETTINGS);
+        Thread.sleep(TIMEOUT);
+        for (String element : textElements) {
+            assertNotNull(element + " item not found under Location Settings",
+                    mDevice.wait(Until.findObject(By.text(element)), TIMEOUT));
+        }
+    }
+
+    @MediumTest
+    public void testLocationSettingsOverflowMenuElements() throws Exception {
+        SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(),
+                            Settings.ACTION_LOCATION_SOURCE_SETTINGS);
+        // Tap on overflow menu
+        mDevice.wait(Until.findObject(By.desc("More options")), TIMEOUT).click();
+        // Verify scanning
+        assertNotNull("Scanning item not found under Location Settings",
+                mDevice.wait(Until.findObject(By.text("Scanning")), TIMEOUT));
+        // Verify help & feedback
+        assertNotNull("Help & feedback item not found under Location Settings",
+                mDevice.wait(Until.findObject(By.text("Help & feedback")), TIMEOUT));
+    }
+
+    private void verifyLocationSettingsMode(int mode) throws Exception {
+        int modeIntValue = 1;
+        String textMode = "Device only";
+        if (mode == Settings.Secure.LOCATION_MODE_HIGH_ACCURACY) {
+            modeIntValue = 3;
+            textMode = "High accuracy";
+        }
+        else if (mode == Settings.Secure.LOCATION_MODE_BATTERY_SAVING) {
+            modeIntValue = 2;
+            textMode = "Battery saving";
+        }
+        // Load location settings
+        SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(),
+                Settings.ACTION_LOCATION_SOURCE_SETTINGS);
+        // Tap on mode
+        mDevice.wait(Until.findObject(By.text("Mode")), TIMEOUT).click();
+        Thread.sleep(TIMEOUT);
+        assertNotNull("Location mode screen not loaded", mDevice.wait(Until.findObject
+                (By.text("Location mode")), TIMEOUT));
+        // Choose said mode
+        mDevice.wait(Until.findObject(By.text(textMode)), TIMEOUT).click();
+        Thread.sleep(TIMEOUT);
+        if (mode == Settings.Secure.LOCATION_MODE_HIGH_ACCURACY ||
+                mode == Settings.Secure.LOCATION_MODE_BATTERY_SAVING) {
+            // Expect another dialog here at improving location tracking
+            // for the first time
+            UiObject2 agreeDialog = mDevice.wait(Until.findObject
+                    (By.text("Improve location accuracy?")), TIMEOUT);
+            if (agreeDialog != null) {
+            mDevice.wait(Until.findObject
+                                (By.text("AGREE")), TIMEOUT).click();
+            Thread.sleep(TIMEOUT);
+            assertNull("Improve location dialog not dismissed", mDevice.wait(Until.findObject
+                    (By.text("Improve location accuracy?")), TIMEOUT));
+            }
+        }
+        // get setting and verify value
+        // Verify change of mode
+        int locationSettingMode =
+                Settings.Secure.getInt(getInstrumentation().getContext().getContentResolver(),
+                Settings.Secure.LOCATION_MODE);
+        assertEquals(mode + " value not set correctly for location.", modeIntValue,
+                locationSettingMode);
+    }
+
+    private void verifyLocationSettingsOnOrOff(boolean verifyOn) throws Exception {
+        // Set location flag
+        if (verifyOn) {
+            Settings.Secure.putInt(getInstrumentation().getContext().getContentResolver(),
+                    Settings.Secure.LOCATION_MODE, Settings.Secure.LOCATION_MODE_OFF);
+        }
+        else {
+            Settings.Secure.putInt(getInstrumentation().getContext().getContentResolver(),
+                    Settings.Secure.LOCATION_MODE, Settings.Secure.LOCATION_MODE_SENSORS_ONLY);
+        }
+        // Load location settings
+        SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(),
+                Settings.ACTION_LOCATION_SOURCE_SETTINGS);
+        // Toggle UI
+        mDevice.wait(Until.findObject(By.res(SETTINGS_PACKAGE, "switch_widget")), TIMEOUT).click();
+        Thread.sleep(TIMEOUT);
+        // Verify change in setting
+        int locationEnabled = Settings.Secure.getInt(getInstrumentation()
+                 .getContext().getContentResolver(),
+                 Settings.Secure.LOCATION_MODE);
+        if (verifyOn) {
+            assertFalse("Location not enabled correctly", locationEnabled == 0);
+        }
+        else {
+            assertEquals("Location not disabled correctly", 0, locationEnabled);
+        }
+    }
+}
diff --git a/tests/functional/settingstests/src/com/android/settings/functional/MainSettingsLargeTests.java b/tests/functional/settingstests/src/com/android/settings/functional/MainSettingsLargeTests.java
new file mode 100644
index 0000000..afc4fe1
--- /dev/null
+++ b/tests/functional/settingstests/src/com/android/settings/functional/MainSettingsLargeTests.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.settings.functional;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.provider.Settings;
+import android.provider.Settings.SettingNotFoundException;
+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.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+
+import junit.framework.Assert;
+
+public class MainSettingsLargeTests extends InstrumentationTestCase {
+    private static final String SETTINGS_PACKAGE = "com.android.settings";
+    private static final int TIMEOUT = 2000;
+    private static final String PERSONAL_CATEGORY = "Personal";
+    private static final String[] sPersonalItems = new String[] {
+            "Location", "Security", "Accounts", "Google", "Languages & input", "Backup & reset"
+    };
+    private static final String SYSTEM_CATEGORY = "System";
+    private static final String[] sSystemItems = new String[] {
+            "Date & time", "Accessibility", "Printing", "About phone"
+    };
+    private UiDevice mDevice;
+    private Context mContext = null;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mDevice = UiDevice.getInstance(getInstrumentation());
+        mContext = getInstrumentation().getContext();
+        try {
+            mDevice.setOrientationNatural();
+        } catch (RemoteException e) {
+            throw new RuntimeException("failed to freeze device orientaion", e);
+        }
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        // Need to finish settings activity
+        mDevice.pressHome();
+        super.tearDown();
+    }
+
+    @LargeTest
+    public void testPersonalSettingCategory() throws Exception {
+        launchMainSettingsCategory(PERSONAL_CATEGORY, sPersonalItems);
+    }
+
+    @LargeTest
+    public void testSystemSettingCategory() throws Exception {
+        launchMainSettingsCategory(SYSTEM_CATEGORY, sSystemItems);
+    }
+
+    private void launchMainSettingsCategory(String category, String[] items) throws Exception {
+        launchMainSettings(Settings.ACTION_SETTINGS);
+        launchSettingItems(category);
+        for (String i : items) {
+            launchSettingItems(i);
+            Log.d(SETTINGS_PACKAGE, String.format("launch setting: %s", i));
+        }
+    }
+
+    private void launchMainSettings(String mainSetting) throws Exception {
+        mDevice.pressHome();
+        Intent settingIntent = new Intent(mainSetting);
+        settingIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        getInstrumentation().getContext().startActivity(settingIntent);
+        Thread.sleep(TIMEOUT * 2);
+    }
+
+    private void launchSettingItems(String title) throws Exception {
+        int maxAttempt = 5;
+        UiObject2 item = null;
+        UiObject2 view = null;
+        while (maxAttempt-- > 0) {
+            item = mDevice.wait(Until.findObject(By.res("android:id/title").text(title)), TIMEOUT);
+            if (item == null) {
+                view = mDevice.wait(
+                        Until.findObject(By.res(SETTINGS_PACKAGE, "main_content")),
+                        TIMEOUT);
+                view.scroll(Direction.DOWN, 1.0f);
+            } else {
+                return;
+            }
+        }
+        assertNotNull(String.format("%s in Setting has not been loaded correctly", title), item);
+    }
+}
\ No newline at end of file
diff --git a/tests/functional/settingstests/src/com/android/settings/functional/MainSettingsTests.java b/tests/functional/settingstests/src/com/android/settings/functional/MainSettingsTests.java
new file mode 100644
index 0000000..eec5f39
--- /dev/null
+++ b/tests/functional/settingstests/src/com/android/settings/functional/MainSettingsTests.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 android.settings.functional;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.platform.test.helpers.SettingsHelperImpl;
+import android.provider.Settings;
+import android.provider.Settings.SettingNotFoundException;
+import android.util.Log;
+import android.view.inputmethod.InputMethodManager;
+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.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+
+import junit.framework.Assert;
+
+/** Component test for verifying basic functionality of Main Setting screen */
+public class MainSettingsTests extends InstrumentationTestCase {
+    private static final String SETTINGS_PACKAGE = "com.android.settings";
+    private static final int TIMEOUT = 2000;
+    private static final String WIFI_CATEGORY = "Wireless & networks";
+    private static final String[] sWifiItems = new String[] {
+            "Wi‑Fi", "Bluetooth", "Data usage", "More"
+    };
+    private static final String DEVICE_CATEGORY = "Device";
+    private static final String[] sDeviceItems = new String[] {
+            "Display", "Notifications", "Sound", "Apps", "Storage", "Battery", "Memory",
+            "Users"
+    };
+    private UiDevice mDevice;
+    private Context mContext = null;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mDevice = UiDevice.getInstance(getInstrumentation());
+        mContext = getInstrumentation().getContext();
+        mDevice.setOrientationNatural();
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        // Need to finish settings activity
+        mDevice.pressHome();
+        mDevice.unfreezeRotation();
+        mDevice.waitForIdle();
+        super.tearDown();
+    }
+
+    @MediumTest
+    public void testLoadSetting() throws Exception {
+        launchMainSettings(Settings.ACTION_SETTINGS);
+        UiObject2 settingHeading = mDevice.wait(Until.findObject(By.text("Settings")),
+                TIMEOUT);
+        assertNotNull("Setting menu has not loaded correctly", settingHeading);
+    }
+
+    @MediumTest
+    public void testWifiSettingCategory() throws Exception {
+        launchMainSettingsCategory(WIFI_CATEGORY, sWifiItems);
+    }
+
+    @MediumTest
+    public void testDeviceSettingCategory() throws Exception {
+        launchMainSettingsCategory(DEVICE_CATEGORY, sDeviceItems);
+    }
+
+    private void launchMainSettingsCategory(String category, String[] items) throws Exception {
+        SettingsHelper.launchSettingsPage(getInstrumentation().getContext(),
+                Settings.ACTION_SETTINGS);
+        launchSettingItems(category);
+        for (String i : items) {
+            launchSettingItems(i);
+            Log.d(SETTINGS_PACKAGE, String.format("launch setting: %s", i));
+        }
+        UiObject2 mainSettings = mDevice.wait(
+                Until.findObject(By.res("com.android.settings:id/main_content")),
+                TIMEOUT);
+        // Scrolling back up twice so we're at the top of the settings list.
+        for ( int i = 0; i < 2; i++) {
+            mainSettings.scroll(Direction.UP, 1.0f);
+        }
+    }
+
+    @MediumTest
+    public void testSearchSetting() throws Exception {
+        launchMainSettings(Settings.ACTION_SETTINGS);
+        mDevice.wait(Until.findObject(By.desc("Search settings")), TIMEOUT).click();
+        UiObject2 searchBox = mDevice.wait(Until.findObject(By.res("android:id/search_src_text")),
+                TIMEOUT);
+        InputMethodManager imm = (InputMethodManager) mContext
+                .getSystemService(Context.INPUT_METHOD_SERVICE);
+        if (!imm.isAcceptingText()) {
+            mDevice.wait(Until.findObject(By.desc("Collapse")), TIMEOUT).click();
+            assertNotNull("Search Setting has not loaded correctly", searchBox);
+        } else {
+            mDevice.wait(Until.findObject(By.desc("Collapse")), TIMEOUT).click();
+        }
+    }
+
+    @MediumTest
+    public void testOverflowSetting() throws Exception {
+        launchMainSettings(Settings.ACTION_SETTINGS);
+        mDevice.wait(Until.findObject(By.desc("More options")), TIMEOUT).click();
+        mDevice.wait(Until.findObject(By.text("Help & feedback")), TIMEOUT).click();
+        UiObject2 help = mDevice.wait(Until.findObject(By.text("Help")),
+                TIMEOUT);
+        assertNotNull("Overflow setting has not loaded correctly", help);
+    }
+
+    private void launchMainSettings(String mainSetting) throws Exception {
+        mDevice.pressHome();
+        Intent settingIntent = new Intent(mainSetting);
+        settingIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        getInstrumentation().getContext().startActivity(settingIntent);
+        Thread.sleep(TIMEOUT * 3);
+    }
+
+    private void launchSettingItems(String title) throws Exception {
+        int maxAttempt = 5;
+        UiObject2 item = null;
+        UiObject2 view = null;
+        while (maxAttempt-- > 0) {
+            item = mDevice.wait(Until.findObject(By.res("android:id/title").text(title)), TIMEOUT);
+            if (item == null) {
+                view = mDevice.wait(
+                        Until.findObject(By.res("com.android.settings:id/main_content")),
+                        TIMEOUT);
+                view.scroll(Direction.DOWN, 1.0f);
+            } else {
+                return;
+            }
+        }
+        assertNotNull(String.format("%s in Setting has not been loaded correctly", title), item);
+    }
+}
diff --git a/tests/functional/settingstests/src/com/android/settings/functional/MoreWirelessSettingsTests.java b/tests/functional/settingstests/src/com/android/settings/functional/MoreWirelessSettingsTests.java
new file mode 100644
index 0000000..e306a3f
--- /dev/null
+++ b/tests/functional/settingstests/src/com/android/settings/functional/MoreWirelessSettingsTests.java
@@ -0,0 +1,147 @@
+/*
+ * 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.settings.functional;
+
+import android.content.Context;
+import android.net.wifi.WifiManager;
+import android.nfc.NfcAdapter;
+import android.nfc.NfcManager;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.platform.test.helpers.SettingsHelperImpl;
+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.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+
+
+public class MoreWirelessSettingsTests extends InstrumentationTestCase {
+
+    private static final String SETTINGS_PACKAGE = "com.android.settings";
+    private static final int TIMEOUT = 2000;
+    private UiDevice mDevice;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mDevice = UiDevice.getInstance(getInstrumentation());
+        try {
+            mDevice.setOrientationNatural();
+        } catch (RemoteException e) {
+            throw new RuntimeException("failed to freeze device orientaion", e);
+        }
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        mDevice.pressBack();
+        mDevice.pressHome();
+        super.tearDown();
+    }
+
+    @MediumTest
+    public void testAirplaneModeEnabled() throws Exception {
+        verifyAirplaneModeOnOrOff(true);
+    }
+
+    @MediumTest
+    public void testAirplaneModeDisabled() throws Exception {
+        verifyAirplaneModeOnOrOff(false);
+    }
+
+    // This NFC toggle test is set up this way since there's no way to set
+    // the NFC flag to enabled or disabled without touching UI.
+    // This way, we get coverage for whether or not the toggle button works.
+    @MediumTest
+    public void testNFCToggle() throws Exception {
+        NfcManager manager = (NfcManager) getInstrumentation().getContext()
+               .getSystemService(Context.NFC_SERVICE);
+        NfcAdapter nfcAdapter = manager.getDefaultAdapter();
+        boolean nfcInitiallyEnabled = nfcAdapter.isEnabled();
+        SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(),
+                Settings.ACTION_WIRELESS_SETTINGS);
+        UiObject2 nfcSetting = mDevice.wait(Until.findObject(By.text("NFC")), TIMEOUT);
+        nfcSetting.click();
+        Thread.sleep(TIMEOUT*2);
+        if (nfcInitiallyEnabled) {
+            assertFalse("NFC wasn't disabled on toggle", nfcAdapter.isEnabled());
+            nfcSetting.click();
+            Thread.sleep(TIMEOUT*2);
+            assertTrue("NFC wasn't enabled on toggle", nfcAdapter.isEnabled());
+        }
+        else {
+            assertTrue("NFC wasn't enabled on toggle", nfcAdapter.isEnabled());
+            nfcSetting.click();
+            Thread.sleep(TIMEOUT*2);
+            assertFalse("NFC wasn't disabled on toggle", nfcAdapter.isEnabled());
+        }
+    }
+
+    @MediumTest
+    public void testTetheringMenuLoad() throws Exception {
+        SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(),
+                Settings.ACTION_WIRELESS_SETTINGS);
+        mDevice.wait(Until
+                 .findObject(By.text("Tethering & portable hotspot")), TIMEOUT)
+                 .click();
+        Thread.sleep(TIMEOUT);
+        UiObject2 usbTethering = mDevice.wait(Until
+                 .findObject(By.text("USB tethering")), TIMEOUT);
+        assertNotNull("Tethering screen did not load correctly", usbTethering);
+    }
+
+    @MediumTest
+    public void testVPNMenuLoad() throws Exception {
+        SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(),
+                Settings.ACTION_WIRELESS_SETTINGS);
+        mDevice.wait(Until
+                 .findObject(By.text("VPN")), TIMEOUT)
+                 .click();
+        Thread.sleep(TIMEOUT);
+        UiObject2 usbTethering = mDevice.wait(Until
+                 .findObject(By.res(SETTINGS_PACKAGE, "vpn_create")), TIMEOUT);
+        assertNotNull("VPN screen did not load correctly", usbTethering);
+    }
+
+    private void verifyAirplaneModeOnOrOff(boolean verifyOn) throws Exception {
+        if (verifyOn) {
+            Settings.Global.putString(getInstrumentation().getContext().getContentResolver(),
+                    Settings.Global.AIRPLANE_MODE_ON, "0");
+        }
+        else {
+            Settings.Global.putString(getInstrumentation().getContext().getContentResolver(),
+                    Settings.Global.AIRPLANE_MODE_ON, "1");
+        }
+        SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(),
+                Settings.ACTION_WIRELESS_SETTINGS);
+        mDevice.wait(Until
+                .findObject(By.text("Airplane mode")), TIMEOUT)
+                .click();
+        Thread.sleep(TIMEOUT);
+        String airplaneModeValue = Settings.Global
+                .getString(getInstrumentation().getContext().getContentResolver(),
+                Settings.Global.AIRPLANE_MODE_ON);
+        if (verifyOn) {
+            assertEquals("1", airplaneModeValue);
+        }
+        else {
+            assertEquals("0", airplaneModeValue);
+        }
+    }
+}
diff --git a/tests/functional/settingstests/src/com/android/settings/functional/QuickSettingsTest.java b/tests/functional/settingstests/src/com/android/settings/functional/QuickSettingsTest.java
new file mode 100644
index 0000000..50cd5b9
--- /dev/null
+++ b/tests/functional/settingstests/src/com/android/settings/functional/QuickSettingsTest.java
@@ -0,0 +1,333 @@
+/*
+ * 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.settings.functional;
+
+import android.bluetooth.BluetoothManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.location.LocationManager;
+import android.net.wifi.WifiManager;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Until;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.util.Log;
+
+public class QuickSettingsTest extends InstrumentationTestCase {
+    private static final String LOG_TAG = QuickSettingsTest.class.getSimpleName();
+    private static final int LONG_TIMEOUT = 2000;
+    private static final int SHORT_TIMEOUT = 500;
+
+    private enum QuickSettingTiles {
+        WIFI("Wi-Fi"), SIM("SIM"), DND("Do not disturb"), FLASHLIGHT("Flashlight"), SCREEN(
+                "Auto-rotate screen"), BLUETOOTH("Bluetooth"), AIRPLANE("Airplane mode"),
+                LOCATION("Location"), BRIGHTNESS("Display brightness");
+
+        private final String name;
+
+        private QuickSettingTiles(String name) {
+            this.name = name;
+        }
+
+        public String getName() {
+            return this.name;
+        }
+    };
+
+    private UiDevice mDevice = null;
+    private ContentResolver mResolver;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mDevice = UiDevice.getInstance(getInstrumentation());
+        getInstrumentation().getContext();
+        mResolver = getInstrumentation().getContext().getContentResolver();
+        mDevice.wakeUp();
+        mDevice.pressHome();
+        mDevice.setOrientationNatural();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        // Need to finish settings activity
+        mDevice.pressHome();
+        mDevice.unfreezeRotation();
+        super.tearDown();
+    }
+
+    @MediumTest
+    public void testQuickSettingDrawDown() throws Exception {
+        mDevice.pressHome();
+        // Draw down once to load quick settings shade only
+        swipeDown();
+        UiObject2 quicksettingsShade = mDevice.wait(
+                Until.findObject(By.res("com.android.systemui:id/expand_indicator")),
+                LONG_TIMEOUT);
+        assertNotNull("Quick settings shade not visible on draw down", quicksettingsShade);
+    }
+
+    @MediumTest
+    public void testQuickSettingExpand() throws Exception {
+        launchQuickSetting();
+        // Verify that the settings object is visible on full expansion
+        UiObject2 quicksettingsExpand = mDevice.wait(Until.findObject(By.desc("Open settings.")),
+                LONG_TIMEOUT);
+        assertNotNull("Quick settings shade did not expand correctly on two swipe downs",
+                quicksettingsExpand);
+    }
+
+    @MediumTest
+    public void testQuickSettingCollapse() throws Exception {
+        launchQuickSetting();
+        // Tap on the expand chevron once more to collapse the QS shade
+        mDevice.wait(Until.findObject(By.res("com.android.systemui:id/expand_indicator")),
+                LONG_TIMEOUT).click();
+
+        // Verify that the brightness slider which is only visible on full expansion
+        // isn't visible in the collapsed state
+        UiObject2 quicksettingsExpandedShade = mDevice.wait(
+                Until.findObject(By.descContains(QuickSettingTiles.BRIGHTNESS.getName())),
+                LONG_TIMEOUT);
+        assertNotNull("Quick settings shade did not collapse correctly",
+                quicksettingsExpandedShade);
+    }
+
+    @MediumTest
+    public void testQuickSettingDismiss() throws Exception {
+        launchQuickSetting();
+        // Swipe up twice to fully dismiss quick settings
+        swipeUp();
+        swipeUp();
+        UiObject2 quicksettingsShade = mDevice.wait(
+                Until.findObject(By.res("com.android.systemui:id/expand_indicator")),
+                SHORT_TIMEOUT);
+        assertNull("Quick settings collapsed shade was not dismissed correctly",
+                quicksettingsShade);
+    }
+
+    @MediumTest
+    public void testQuickSettingTilesVisible() throws Exception {
+        launchQuickSetting();
+        Thread.sleep(LONG_TIMEOUT);
+        for (QuickSettingTiles tile : QuickSettingTiles.values()) {
+            UiObject2 quickSettingTile = mDevice.wait(
+                    Until.findObject(By.descContains(tile.getName())),
+                    SHORT_TIMEOUT);
+            assertNotNull(String.format("%s did not load correctly", tile.getName()),
+                    quickSettingTile);
+        }
+    }
+
+    @MediumTest
+    public void testQuickSettingWifiEnabled() throws Exception {
+        verifyWiFiOnOrOff(true);
+    }
+
+    @MediumTest
+    public void testQuickSettingWifiDisabled() throws Exception {
+        verifyWiFiOnOrOff(false);
+    }
+
+    private void verifyWiFiOnOrOff(boolean verifyOn) throws Exception {
+        String airPlaneMode = Settings.Global.getString(
+                mResolver,
+                Settings.Global.AIRPLANE_MODE_ON);
+        try {
+            Settings.Global.putString(mResolver,
+                    Settings.Global.AIRPLANE_MODE_ON, "0");
+            Thread.sleep(LONG_TIMEOUT);
+            WifiManager wifiManager = (WifiManager) getInstrumentation().getContext()
+                    .getSystemService(Context.WIFI_SERVICE);
+            wifiManager.setWifiEnabled(!verifyOn);
+            launchQuickSetting();
+            mDevice.wait(Until.findObject(By.descContains(QuickSettingTiles.WIFI.getName())),
+                    LONG_TIMEOUT).click();
+            if (verifyOn) {
+                mDevice.pressBack();
+            } else {
+                mDevice.wait(Until.findObject(By.res("android:id/toggle")), LONG_TIMEOUT).click();
+            }
+            Thread.sleep(LONG_TIMEOUT);
+            String changedWifiValue = Settings.Global.getString(mResolver, Settings.Global.WIFI_ON);
+            Thread.sleep(LONG_TIMEOUT);
+            if (verifyOn) {
+                assertEquals("1", changedWifiValue);
+            } else {
+                assertEquals("0", changedWifiValue);
+            }
+        } finally {
+            Settings.Global.putString(getInstrumentation().getContext().getContentResolver(),
+                    Settings.Global.AIRPLANE_MODE_ON, airPlaneMode);
+        }
+    }
+
+    @MediumTest
+    public void testQuickSettingBluetoothEnabled() throws Exception {
+        verifyBluetoothOnOrOff(true);
+    }
+
+    @MediumTest
+    public void testQuickSettingBluetoothDisabled() throws Exception {
+        verifyBluetoothOnOrOff(false);
+    }
+
+    private void verifyBluetoothOnOrOff(boolean verifyOn) throws Exception {
+        BluetoothManager bluetoothManager = (BluetoothManager) getInstrumentation().getContext()
+                .getSystemService(Context.BLUETOOTH_SERVICE);
+        if (!verifyOn) {
+            bluetoothManager.getAdapter().enable();
+        } else {
+            bluetoothManager.getAdapter().disable();
+        }
+        launchQuickSetting();
+        mDevice.wait(Until.findObject(By.textContains(QuickSettingTiles.BLUETOOTH.getName())),
+                LONG_TIMEOUT).click();
+        if (verifyOn) {
+            mDevice.pressBack();
+        } else {
+            mDevice.wait(Until.findObject(By.res("android:id/toggle")), LONG_TIMEOUT).click();
+        }
+        Thread.sleep(LONG_TIMEOUT);
+        String bluetoothVal = Settings.Global.getString(
+                mResolver,
+                Settings.Global.BLUETOOTH_ON);
+        if (verifyOn) {
+            assertEquals("1", bluetoothVal);
+        } else {
+            assertEquals("0", bluetoothVal);
+        }
+    }
+
+    @MediumTest
+    public void testQuickSettingFlashLight() throws Exception {
+        String lightOn = "On";
+        String lightOff = "Off";
+        boolean verifyOn = false;
+        launchQuickSetting();
+        UiObject2 flashLight = mDevice.wait(
+                Until.findObject(By.descContains(QuickSettingTiles.FLASHLIGHT.getName())),
+                LONG_TIMEOUT);
+        if (flashLight.getText().equals(lightOn)) {
+            verifyOn = true;
+        }
+        mDevice.wait(Until.findObject(By.textContains(QuickSettingTiles.FLASHLIGHT.getName())),
+                LONG_TIMEOUT).click();
+        Thread.sleep(LONG_TIMEOUT);
+        flashLight = mDevice.wait(
+                Until.findObject(By.descContains(QuickSettingTiles.FLASHLIGHT.getName())),
+                LONG_TIMEOUT);
+        if (verifyOn) {
+            assertTrue(flashLight.getText().equals(lightOff));
+        } else {
+            assertTrue(flashLight.getText().equals(lightOn));
+            mDevice.wait(Until.findObject(By.textContains(QuickSettingTiles.FLASHLIGHT.getName())),
+                    LONG_TIMEOUT).click();
+        }
+    }
+
+    @MediumTest
+    public void testQuickSettingDND() throws Exception {
+        int onSetting = Settings.Global.getInt(mResolver, "zen_mode");
+        launchQuickSetting();
+        mDevice.wait(Until.findObject(By.descContains(QuickSettingTiles.DND.getName())),
+                LONG_TIMEOUT).click();
+        if (onSetting == 0) {
+            mDevice.pressBack();
+        }
+        Thread.sleep(LONG_TIMEOUT);
+        int changedSetting = Settings.Global.getInt(mResolver, "zen_mode");
+        assertFalse(onSetting == changedSetting);
+    }
+
+    @MediumTest
+    public void testQuickSettingAirplaneMode() throws Exception {
+        int onSetting = Integer.parseInt(Settings.Global.getString(
+                mResolver,
+                Settings.Global.AIRPLANE_MODE_ON));
+        try {
+            launchQuickSetting();
+            mDevice.wait(Until.findObject(By.descContains(QuickSettingTiles.AIRPLANE.getName())),
+                    LONG_TIMEOUT).click();
+            Thread.sleep(LONG_TIMEOUT);
+            int changedSetting = Integer.parseInt(Settings.Global.getString(
+                    mResolver,
+                    Settings.Global.AIRPLANE_MODE_ON));
+            assertTrue((1 - onSetting) == changedSetting);
+        } finally {
+            Settings.Global.putString(getInstrumentation().getContext().getContentResolver(),
+                    Settings.Global.AIRPLANE_MODE_ON, Integer.toString(onSetting));
+        }
+    }
+
+    @MediumTest
+    public void testQuickSettingOrientation() throws Exception {
+        launchQuickSetting();
+        mDevice.wait(Until.findObject(By.descContains(QuickSettingTiles.SCREEN.getName())),
+                LONG_TIMEOUT).click();
+        Thread.sleep(LONG_TIMEOUT);
+        String rotation = Settings.System.getString(mResolver,
+                Settings.System.ACCELEROMETER_ROTATION);
+        assertEquals("1", rotation);
+    }
+
+    @MediumTest
+    public void testQuickSettingLocation() throws Exception {
+        LocationManager service = (LocationManager) getInstrumentation().getContext()
+                .getSystemService(Context.LOCATION_SERVICE);
+        boolean onSetting = service.isProviderEnabled(LocationManager.GPS_PROVIDER);
+        try {
+            launchQuickSetting();
+            mDevice.wait(Until.findObject(By.descContains(QuickSettingTiles.LOCATION.getName())),
+                    LONG_TIMEOUT).click();
+            Thread.sleep(LONG_TIMEOUT);
+            boolean changedSetting = service.isProviderEnabled(LocationManager.GPS_PROVIDER);
+            assertTrue(onSetting == !changedSetting);
+        } finally {
+            mDevice.wait(Until.findObject(By.descContains(QuickSettingTiles.LOCATION.getName())),
+                    LONG_TIMEOUT).click();
+        }
+    }
+
+    private void launchQuickSetting() throws Exception {
+        mDevice.pressHome();
+        swipeDown();
+        Thread.sleep(LONG_TIMEOUT);
+        swipeDown();
+    }
+
+    private void swipeUp() throws Exception {
+        mDevice.swipe(mDevice.getDisplayWidth() / 2, mDevice.getDisplayHeight(),
+                mDevice.getDisplayWidth() / 2, 0, 30);
+        Thread.sleep(SHORT_TIMEOUT);
+    }
+
+    private void swipeDown() throws Exception {
+        mDevice.swipe(mDevice.getDisplayWidth() / 2, 0, mDevice.getDisplayWidth() / 2,
+                mDevice.getDisplayHeight() / 2 + 50, 20);
+        Thread.sleep(SHORT_TIMEOUT);
+    }
+
+    private void swipeLeft() {
+        mDevice.swipe(mDevice.getDisplayWidth() / 2, mDevice.getDisplayHeight() / 2, 0,
+                mDevice.getDisplayHeight() / 2, 5);
+    }
+}
diff --git a/tests/functional/settingstests/src/com/android/settings/functional/SettingsHelper.java b/tests/functional/settingstests/src/com/android/settings/functional/SettingsHelper.java
new file mode 100644
index 0000000..58875d2
--- /dev/null
+++ b/tests/functional/settingstests/src/com/android/settings/functional/SettingsHelper.java
@@ -0,0 +1,140 @@
+package android.settings.functional;
+
+import android.app.Instrumentation;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.provider.Settings;
+import android.provider.Settings.SettingNotFoundException;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.Until;
+
+import java.util.regex.Pattern;
+
+public class SettingsHelper {
+
+    public static final String PKG = "com.android.settings";
+    private static final int TIMEOUT = 2000;
+
+    private UiDevice mDevice;
+    private Instrumentation mInst;
+    private ContentResolver mResolver;
+
+    public SettingsHelper(UiDevice device, Instrumentation inst) {
+        mDevice = device;
+        mInst = inst;
+        mResolver = inst.getContext().getContentResolver();
+    }
+
+    public static enum SettingsType {
+        SYSTEM,
+        SECURE,
+        GLOBAL
+    }
+
+    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);
+    }
+
+    public void clickSetting(String settingName) {
+        mDevice.wait(Until.findObject(By.text(settingName)), TIMEOUT).click();
+    }
+
+    public void clickSetting(Pattern settingName) {
+        mDevice.wait(Until.findObject(By.text(settingName)), TIMEOUT).click();
+    }
+
+    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);
+    }
+
+    public boolean verifyToggleSetting(SettingsType type, String settingAction,
+            String settingName, String internalName) throws Exception {
+        return verifyToggleSetting(
+                type, settingAction, Pattern.compile(settingName), internalName, true);
+    }
+
+    public boolean verifyToggleSetting(SettingsType type, String settingAction,
+            Pattern settingName, String internalName) throws Exception {
+        return verifyToggleSetting(type, settingAction, settingName, internalName, true);
+    }
+
+    public boolean verifyToggleSetting(SettingsType type, String settingAction,
+            String settingName, String internalName, boolean doLaunch) throws Exception {
+        return verifyToggleSetting(
+                type, settingAction, Pattern.compile(settingName), internalName, doLaunch);
+    }
+
+    public boolean verifyToggleSetting(SettingsType type, String settingAction,
+            Pattern settingName, String internalName, boolean doLaunch) throws Exception {
+        int onSetting = Integer.parseInt(getStringSetting(type, internalName));
+        if (doLaunch) {
+            launchSettingsPage(mInst.getContext(), settingAction);
+        }
+        clickSetting(settingName);
+        Thread.sleep(1000);
+        String changedSetting = getStringSetting(type, internalName);
+        return (1 - onSetting) == Integer.parseInt(changedSetting);
+    }
+
+    public boolean verifyRadioSetting(SettingsType type, String settingAction,
+            String baseName, String settingName,
+            String internalName, String testVal) throws Exception {
+        clickSetting(baseName);
+        clickSetting(settingName);
+        Thread.sleep(500);
+        return getStringSetting(type, internalName).equals(testVal);
+    }
+
+    private void putStringSetting(SettingsType type, String sName, String value) {
+        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;
+        }
+    }
+
+    private 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 "";
+    }
+
+    private void putIntSetting(SettingsType type, String sName, int value) {
+        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;
+        }
+    }
+
+    private 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;
+    }
+}
diff --git a/tests/functional/settingstests/src/com/android/settings/functional/SoundSettingsTest.java b/tests/functional/settingstests/src/com/android/settings/functional/SoundSettingsTest.java
new file mode 100644
index 0000000..ee4952c
--- /dev/null
+++ b/tests/functional/settingstests/src/com/android/settings/functional/SoundSettingsTest.java
@@ -0,0 +1,277 @@
+package android.settings.functional;
+
+import android.app.NotificationManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.provider.Settings;
+import android.service.notification.ZenModeConfig;
+import android.platform.test.helpers.SettingsHelperImpl;
+import android.platform.test.helpers.SettingsHelperImpl.SettingsType;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.Until;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.test.suitebuilder.annotation.Suppress;
+
+import com.android.server.notification.ConditionProviders;
+import com.android.server.notification.ManagedServices.UserProfiles;
+import com.android.server.notification.ZenModeHelper;
+
+public class SoundSettingsTest extends InstrumentationTestCase {
+    private static final String PAGE = Settings.ACTION_SOUND_SETTINGS;
+    private static final int TIMEOUT = 2000;
+
+    private UiDevice mDevice;
+    private ContentResolver mResolver;
+    private SettingsHelperImpl mHelper;
+    private ZenModeHelper mZenHelper;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mDevice = UiDevice.getInstance(getInstrumentation());
+        mDevice.setOrientationNatural();
+        mResolver = getInstrumentation().getContext().getContentResolver();
+        mHelper = new SettingsHelperImpl(getInstrumentation());
+        ConditionProviders cps = new ConditionProviders(
+                getInstrumentation().getContext(), new Handler(), new UserProfiles());
+        mZenHelper = new ZenModeHelper(getInstrumentation().getContext(),
+                getInstrumentation().getContext().getMainLooper(),
+                cps);
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        mDevice.unfreezeRotation();
+        super.tearDown();
+    }
+
+    @MediumTest
+    public void testCallVibrate() throws Exception {
+        assertTrue(mHelper.verifyToggleSetting(SettingsType.SYSTEM, PAGE,
+                "Also vibrate for calls", Settings.System.VIBRATE_WHEN_RINGING));
+        assertTrue(mHelper.verifyToggleSetting(SettingsType.SYSTEM, PAGE,
+                "Also vibrate for calls", Settings.System.VIBRATE_WHEN_RINGING));
+    }
+
+    @MediumTest
+    public void testOtherSounds() throws Exception {
+        SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+        mHelper.scrollVert(false);
+        Thread.sleep(1000);
+        mHelper.clickSetting("Other sounds");
+        Thread.sleep(1000);
+        try {
+            assertTrue("Dial pad tones not toggled", mHelper.verifyToggleSetting(
+                    SettingsType.SYSTEM, PAGE, "Dial pad tones",
+                    Settings.System.DTMF_TONE_WHEN_DIALING));
+            assertTrue("Screen locking sounds not toggled",
+                    mHelper.verifyToggleSetting(SettingsType.SYSTEM, PAGE,
+                    "Screen locking sounds", Settings.System.LOCKSCREEN_SOUNDS_ENABLED));
+            assertTrue("Charging sounds not toggled",
+                    mHelper.verifyToggleSetting(SettingsType.GLOBAL, PAGE,
+                    "Charging sounds", Settings.Global.CHARGING_SOUNDS_ENABLED));
+            assertTrue("Touch sounds not toggled",
+                    mHelper.verifyToggleSetting(SettingsType.SYSTEM, PAGE,
+                    "Touch sounds", Settings.System.SOUND_EFFECTS_ENABLED));
+            assertTrue("Vibrate on tap not toggled",
+                    mHelper.verifyToggleSetting(SettingsType.SYSTEM, PAGE,
+                    "Vibrate on tap", Settings.System.HAPTIC_FEEDBACK_ENABLED));
+        } finally {
+            mDevice.pressBack();
+        }
+    }
+
+    @MediumTest
+    @Suppress
+    public void testDndPriorityAllows() throws Exception {
+        SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+        Context ctx = getInstrumentation().getContext();
+        try {
+            mHelper.clickSetting("Do not disturb");
+            try {
+                mHelper.clickSetting("Priority only allows");
+                ZenModeConfig baseZenCfg = mZenHelper.getConfig();
+
+                mHelper.clickSetting("Reminders");
+                mHelper.clickSetting("Events");
+                mHelper.clickSetting("Repeat callers");
+
+                ZenModeConfig changedCfg = mZenHelper.getConfig();
+                assertFalse(baseZenCfg.allowReminders == changedCfg.allowReminders);
+                assertFalse(baseZenCfg.allowEvents == changedCfg.allowEvents);
+                assertFalse(baseZenCfg.allowRepeatCallers == changedCfg.allowRepeatCallers);
+
+                mHelper.clickSetting("Reminders");
+                mHelper.clickSetting("Events");
+                mHelper.clickSetting("Repeat callers");
+
+                changedCfg = mZenHelper.getConfig();
+                assertTrue(baseZenCfg.allowReminders == changedCfg.allowReminders);
+                assertTrue(baseZenCfg.allowEvents == changedCfg.allowEvents);
+                assertTrue(baseZenCfg.allowRepeatCallers == changedCfg.allowRepeatCallers);
+
+                mHelper.clickSetting("Messages");
+                mHelper.clickSetting("From anyone");
+                mHelper.clickSetting("Calls");
+                mHelper.clickSetting("From anyone");
+
+                changedCfg = mZenHelper.getConfig();
+                assertFalse(baseZenCfg.allowCallsFrom == changedCfg.allowCallsFrom);
+                assertFalse(baseZenCfg.allowMessagesFrom == changedCfg.allowMessagesFrom);
+            } finally {
+                mDevice.pressBack();
+            }
+        } finally {
+            mDevice.pressHome();
+        }
+    }
+
+    @MediumTest
+    @Suppress
+    public void testDndVisualInterruptions() throws Exception {
+        SettingsHelper.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+        try {
+            mHelper.clickSetting("Do not disturb");
+            try {
+                mHelper.clickSetting("Visual interruptions");
+                ZenModeConfig baseZenCfg = mZenHelper.getConfig();
+
+                mHelper.clickSetting("Block when screen is on");
+                mHelper.clickSetting("Block when screen is off");
+
+                ZenModeConfig changedCfg = mZenHelper.getConfig();
+                assertFalse(baseZenCfg.allowWhenScreenOff == changedCfg.allowWhenScreenOff);
+                assertFalse(baseZenCfg.allowWhenScreenOn == changedCfg.allowWhenScreenOn);
+
+                mHelper.clickSetting("Block when screen is on");
+                mHelper.clickSetting("Block when screen is off");
+
+                changedCfg = mZenHelper.getConfig();
+                assertTrue(baseZenCfg.allowWhenScreenOff == changedCfg.allowWhenScreenOff);
+                assertTrue(baseZenCfg.allowWhenScreenOn == changedCfg.allowWhenScreenOn);
+            } finally {
+                mDevice.pressBack();
+            }
+        } finally {
+            mDevice.pressBack();
+        }
+    }
+
+    /*
+     * Rather than verifying every ringtone, verify the ones least likely to change
+     * (None and Hangouts) and an arbitrary one from the ringtone pool.
+     */
+    @MediumTest
+    public void testPhoneRingtoneNone() throws Exception {
+        SettingsHelper.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+        mHelper.clickSetting("Phone ringtone");
+        verifyRingtone(new RingtoneSetting("None", "null"),
+                Settings.System.RINGTONE, ScrollDir.UP);
+    }
+
+    @MediumTest
+    @Suppress
+    public void testPhoneRingtoneHangouts() throws Exception {
+        SettingsHelper.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+        mHelper.clickSetting("Phone ringtone");
+        verifyRingtone(new RingtoneSetting("Hangouts Call", "31"), Settings.System.RINGTONE);
+    }
+
+    @MediumTest
+    public void testPhoneRingtoneUmbriel() throws Exception {
+        SettingsHelper.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+        mHelper.clickSetting("Phone ringtone");
+        verifyRingtone(new RingtoneSetting("Umbriel", "49"),
+                Settings.System.RINGTONE, ScrollDir.DOWN);
+    }
+
+    @MediumTest
+    public void testNotificationRingtoneNone() throws Exception {
+        SettingsHelper.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+        mHelper.clickSetting("Default notification ringtone");
+        verifyRingtone(new RingtoneSetting("None", "null"),
+                Settings.System.NOTIFICATION_SOUND, ScrollDir.UP);
+    }
+
+    @MediumTest
+    @Suppress
+    public void testNotificationRingtoneHangouts() throws Exception {
+        SettingsHelper.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+        mHelper.clickSetting("Default notification ringtone");
+        verifyRingtone(new RingtoneSetting("Hangouts Message", "30"),
+                Settings.System.NOTIFICATION_SOUND);
+    }
+
+    @MediumTest
+    public void testNotificationRingtoneTitan() throws Exception {
+        SettingsHelper.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+        mHelper.clickSetting("Default notification ringtone");
+        verifyRingtone(new RingtoneSetting("Titan", "35"),
+                Settings.System.NOTIFICATION_SOUND, ScrollDir.DOWN);
+    }
+
+    @MediumTest
+    public void testAlarmRingtoneNone() throws Exception {
+        SettingsHelper.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+        mHelper.clickSetting("Default alarm ringtone");
+        verifyRingtone(new RingtoneSetting("None", "null"),
+                Settings.System.ALARM_ALERT, ScrollDir.UP);
+    }
+
+    @MediumTest
+    public void testAlarmRingtoneXenon() throws Exception {
+        SettingsHelper.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+        mHelper.clickSetting("Default alarm ringtone");
+        verifyRingtone(new RingtoneSetting("Xenon", "22"),
+                Settings.System.ALARM_ALERT, ScrollDir.DOWN);
+    }
+
+    private void verifyRingtone(RingtoneSetting r, String settingName) {
+        verifyRingtone(r, settingName, ScrollDir.NOSCROLL);
+    }
+
+    private void verifyRingtone(RingtoneSetting r, String settingName, ScrollDir dir) {
+        if (dir != ScrollDir.NOSCROLL) {
+            mHelper.scrollVert(dir == ScrollDir.UP);
+            SystemClock.sleep(1000);
+        }
+        mDevice.wait(Until.findObject(By.text(r.getName())), TIMEOUT).click();
+        mDevice.wait(Until.findObject(By.text("OK")), TIMEOUT).click();
+        SystemClock.sleep(1000);
+        if (r.getVal().equals("null")) {
+            assertEquals(null,
+                    Settings.System.getString(mResolver, settingName));
+        } else if (r.getName().contains("Hangouts")) {
+            assertEquals("content://media/external/audio/media/" + r.getVal(),
+                    Settings.System.getString(mResolver, settingName));
+        } else {
+            assertEquals("content://media/internal/audio/media/" + r.getVal(),
+                    Settings.System.getString(mResolver, settingName));
+        }
+    }
+
+    private enum ScrollDir {
+        UP,
+        DOWN,
+        NOSCROLL
+    }
+
+    class RingtoneSetting {
+        private final String mName;
+        private final String mMediaVal;
+        public RingtoneSetting(String name, String fname) {
+            mName = name;
+            mMediaVal = fname;
+        }
+        public String getName() {
+            return mName;
+        }
+        public String getVal() {
+            return mMediaVal;
+        }
+    }
+}
diff --git a/tests/functional/settingstests/src/com/android/settings/functional/WirelessNetworkSettingsTests.java b/tests/functional/settingstests/src/com/android/settings/functional/WirelessNetworkSettingsTests.java
new file mode 100644
index 0000000..fca0149
--- /dev/null
+++ b/tests/functional/settingstests/src/com/android/settings/functional/WirelessNetworkSettingsTests.java
@@ -0,0 +1,760 @@
+/*
+ * 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.settings.functional;
+
+import android.content.Context;
+import android.net.wifi.WifiManager;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.platform.test.helpers.SettingsHelperImpl;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.StaleObjectException;
+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.MediumTest;
+
+public class WirelessNetworkSettingsTests extends InstrumentationTestCase {
+    // These back button presses are performed in tearDown() to exit Wifi
+    // Settings sub-menus that a test might finish in. This number should be
+    // high enough to account for the deepest sub-menu a test might enter.
+    private static final int NUM_BACK_BUTTON_PRESSES = 5;
+    private static final int TIMEOUT = 2000;
+    private static final int SLEEP_TIME = 500;
+
+    // Note: The values of these variables might affect flakiness in tests that involve
+    // scrolling. Adjust where necessary.
+    private static final float SCROLL_UP_PERCENT = 10.0f;
+    private static final float SCROLL_DOWN_PERCENT = 0.5f;
+    private static final int MAX_SCROLL_ATTEMPTS = 10;
+    private static final int MAX_ADD_NETWORK_BUTTON_ATTEMPTS = 3;
+    private static final int SCROLL_SPEED = 2000;
+
+    private static final String TEST_SSID = "testSsid";
+    private static final String TEST_PW_GE_8_CHAR = "testPasswordGreaterThan8Char";
+    private static final String TEST_PW_LT_8_CHAR = "lt8Char";
+    private static final String TEST_DOMAIN = "testDomain.com";
+
+    private static final String SETTINGS_PACKAGE = "com.android.settings";
+
+    private static final String CHECKBOX_CLASS = "android.widget.CheckBox";
+    private static final String SPINNER_CLASS = "android.widget.Spinner";
+    private static final String EDIT_TEXT_CLASS = "android.widget.EditText";
+    private static final String SCROLLVIEW_CLASS = "android.widget.ScrollView";
+    private static final String LISTVIEW_CLASS = "android.widget.ListView";
+
+    private static final String ADD_NETWORK_MENU_CANCEL_BUTTON_TEXT = "CANCEL";
+    private static final String ADD_NETWORK_MENU_SAVE_BUTTON_TEXT = "SAVE";
+    private static final String ADD_NETWORK_PREFERENCE_TEXT = "Add network";
+    private static final String CACERT_MENU_PLEASE_SELECT_TEXT = "Please select";
+    private static final String CACERT_MENU_USE_SYSTEM_CERTS_TEXT = "Use system certificates";
+    private static final String CACERT_MENU_DO_NOT_VALIDATE_TEXT = "Do not validate";
+    private static final String USERCERT_MENU_PLEASE_SELECT_TEXT = "Please select";
+    private static final String USERCERT_MENU_DO_NOT_PROVIDE_TEXT = "Do not provide";
+    private static final String SECURITY_OPTION_NONE_TEXT = "None";
+    private static final String SECURITY_OPTION_WEP_TEXT = "WEP";
+    private static final String SECURITY_OPTION_PSK_TEXT = "WPA/WPA2 PSK";
+    private static final String SECURITY_OPTION_EAP_TEXT = "802.1x EAP";
+    private static final String EAP_METHOD_PEAP_TEXT = "PEAP";
+    private static final String EAP_METHOD_TLS_TEXT = "TLS";
+    private static final String EAP_METHOD_TTLS_TEXT = "TTLS";
+    private static final String EAP_METHOD_PWD_TEXT = "PWD";
+    private static final String EAP_METHOD_SIM_TEXT = "SIM";
+    private static final String EAP_METHOD_AKA_TEXT = "AKA";
+    private static final String EAP_METHOD_AKA_PRIME_TEXT = "AKA'";
+    private static final String PHASE2_MENU_NONE_TEXT = "None";
+    private static final String PHASE2_MENU_MSCHAPV2_TEXT = "MSCHAPV2";
+    private static final String PHASE2_MENU_GTC_TEXT = "GTC";
+
+    private static final String ADD_NETWORK_MENU_ADV_TOGGLE_RES_ID = "wifi_advanced_togglebox";
+    private static final String ADD_NETWORK_MENU_IP_SETTINGS_RES_ID = "ip_settings";
+    private static final String ADD_NETWORK_MENU_PROXY_SETTINGS_RES_ID = "proxy_settings";
+    private static final String ADD_NETWORK_MENU_SECURITY_OPTION_RES_ID = "security";
+    private static final String ADD_NETWORK_MENU_EAP_METHOD_RES_ID = "method";
+    private static final String ADD_NETWORK_MENU_SSID_RES_ID = "ssid";
+    private static final String ADD_NETWORK_MENU_PHASE2_RES_ID = "phase2";
+    private static final String ADD_NETWORK_MENU_CACERT_RES_ID = "ca_cert";
+    private static final String ADD_NETWORK_MENU_USERCERT_RES_ID = "user_cert";
+    private static final String ADD_NETWORK_MENU_NO_DOMAIN_WARNING_RES_ID = "no_domain_warning";
+    private static final String ADD_NETWORK_MENU_NO_CACERT_WARNING_RES_ID = "no_ca_cert_warning";
+    private static final String ADD_NETWORK_MENU_DOMAIN_LAYOUT_RES_ID = "l_domain";
+    private static final String ADD_NETWORK_MENU_DOMAIN_RES_ID = "domain";
+    private static final String ADD_NETWORK_MENU_IDENTITY_LAYOUT_RES_ID = "l_identity";
+    private static final String ADD_NETWORK_MENU_ANONYMOUS_LAYOUT_RES_ID = "l_anonymous";
+    private static final String ADD_NETWORK_MENU_PASSWORD_LAYOUT_RES_ID = "password_layout";
+    private static final String ADD_NETWORK_MENU_SHOW_PASSWORD_LAYOUT_RES_ID =
+            "show_password_layout";
+    private static final String ADD_NETWORK_MENU_PASSWORD_RES_ID = "password";
+
+    private static final BySelector ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR =
+            By.scrollable(true).clazz(SCROLLVIEW_CLASS);
+    private static final BySelector SPINNER_OPTIONS_SCROLLABLE_BY_SELECTOR =
+            By.scrollable(true).clazz(LISTVIEW_CLASS);
+
+    private UiDevice mDevice;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mDevice = UiDevice.getInstance(getInstrumentation());
+        try {
+            mDevice.setOrientationNatural();
+        } catch (RemoteException e) {
+            throw new RuntimeException("failed to freeze device orientation", e);
+        }
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        // Exit all settings sub-menus.
+        for (int i = 0; i < NUM_BACK_BUTTON_PRESSES; ++i) {
+            mDevice.pressBack();
+        }
+        mDevice.pressHome();
+        super.tearDown();
+    }
+
+    @MediumTest
+    public void testWiFiEnabled() throws Exception {
+        verifyWiFiOnOrOff(true);
+    }
+
+    @MediumTest
+    public void testWiFiDisabled() throws Exception {
+        verifyWiFiOnOrOff(false);
+    }
+
+    @MediumTest
+    public void testWifiMenuLoadConfigure() throws Exception {
+        loadWiFiConfigureMenu();
+        Thread.sleep(SLEEP_TIME);
+        UiObject2 configureWiFiHeading = mDevice.wait(Until.findObject(By.text("Configure Wi‑Fi")),
+                TIMEOUT);
+        assertNotNull("Configure WiFi menu has not loaded correctly", configureWiFiHeading);
+    }
+
+    @MediumTest
+    public void testNetworkNotificationsOn() throws Exception {
+        verifyNetworkNotificationsOnOrOff(true);
+    }
+
+    @MediumTest
+    public void testNetworkNotificationsOff() throws Exception {
+        verifyNetworkNotificationsOnOrOff(false);
+    }
+
+    @MediumTest
+    public void testKeepWiFiDuringSleepAlways() throws Exception {
+        // Change the default and then change it back
+        Settings.Global.putInt(getInstrumentation().getContext().getContentResolver(),
+                Settings.Global.WIFI_SLEEP_POLICY, Settings.Global.WIFI_SLEEP_POLICY_DEFAULT);
+        verifyKeepWiFiOnDuringSleep("Always", Settings.Global.WIFI_SLEEP_POLICY_NEVER);
+    }
+
+    @MediumTest
+    public void testKeepWiFiDuringSleepOnlyWhenPluggedIn() throws Exception {
+        verifyKeepWiFiOnDuringSleep("Only when plugged in",
+                Settings.Global.WIFI_SLEEP_POLICY_NEVER_WHILE_PLUGGED);
+    }
+
+    @MediumTest
+    public void testKeepWiFiDuringSleepNever() throws Exception {
+        verifyKeepWiFiOnDuringSleep("Never", Settings.Global.WIFI_SLEEP_POLICY_DEFAULT);
+    }
+
+    @MediumTest
+    public void testAddNetworkMenu_Default() throws Exception {
+        loadAddNetworkMenu();
+
+        // Submit button should be disabled by default, while cancel button should be enabled.
+        assertFalse(mDevice.wait(Until.findObject(
+                By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+        assertTrue(mDevice.wait(Until.findObject(
+                By.text(ADD_NETWORK_MENU_CANCEL_BUTTON_TEXT)), TIMEOUT).isEnabled());
+
+        // Check that the SSID field is defaults to the hint.
+        assertEquals("Enter the SSID", mDevice.wait(Until.findObject(By
+                .res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_SSID_RES_ID)
+                .clazz(EDIT_TEXT_CLASS)), TIMEOUT)
+                .getText());
+
+        // Check Security defaults to None.
+        assertEquals("None", mDevice.wait(Until.findObject(By
+                .res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_SECURITY_OPTION_RES_ID)
+                .clazz(SPINNER_CLASS)), TIMEOUT)
+                .getChildren().get(0).getText());
+
+        // Check advanced options are collapsed by default.
+        assertFalse(mDevice.wait(Until.findObject(By
+                .res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_ADV_TOGGLE_RES_ID)
+                .clazz(CHECKBOX_CLASS)), TIMEOUT).isChecked());
+
+    }
+
+    @MediumTest
+    public void testAddNetworkMenu_Proxy() throws Exception {
+        loadAddNetworkMenu();
+
+        // Toggle advanced options.
+        mDevice.wait(Until.findObject(By
+                .res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_ADV_TOGGLE_RES_ID)
+                .clazz(CHECKBOX_CLASS)), TIMEOUT).click();
+
+        // Verify Proxy defaults to None.
+        BySelector proxySettingsBySelector =
+                By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_PROXY_SETTINGS_RES_ID)
+                .clazz(SPINNER_CLASS);
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR, proxySettingsBySelector);
+        assertEquals("None", mDevice.wait(Until.findObject(proxySettingsBySelector), TIMEOUT)
+                .getChildren().get(0).getText());
+
+        // Verify that Proxy Manual fields appear.
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR, proxySettingsBySelector);
+        mDevice.wait(Until.findObject(proxySettingsBySelector), TIMEOUT).click();
+        mDevice.wait(Until.findObject(By.text("Manual")), TIMEOUT).click();
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+                By.res(SETTINGS_PACKAGE, "proxy_warning_limited_support"));
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+                By.res(SETTINGS_PACKAGE, "proxy_hostname"));
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+                By.res(SETTINGS_PACKAGE, "proxy_exclusionlist"));
+
+        // Verify that Proxy Auto-Config options appear.
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR, proxySettingsBySelector);
+        mDevice.wait(Until.findObject(proxySettingsBySelector), TIMEOUT).click();
+        mDevice.wait(Until.findObject(By.text("Proxy Auto-Config")), TIMEOUT).click();
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+                By.res(SETTINGS_PACKAGE, "proxy_pac"));
+    }
+
+    @MediumTest
+    public void testAddNetworkMenu_IpSettings() throws Exception {
+        loadAddNetworkMenu();
+
+        // Toggle advanced options.
+        mDevice.wait(Until.findObject(By
+                .res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_ADV_TOGGLE_RES_ID)
+                .clazz(CHECKBOX_CLASS)), TIMEOUT).click();
+
+        // Verify IP settings defaults to DHCP.
+        BySelector ipSettingsBySelector =
+                By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_IP_SETTINGS_RES_ID).clazz(SPINNER_CLASS);
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR, ipSettingsBySelector);
+        assertEquals("DHCP", mDevice.wait(Until.findObject(ipSettingsBySelector), TIMEOUT)
+                .getChildren().get(0).getText());
+
+        // Verify that Static IP settings options appear.
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR, ipSettingsBySelector).click();
+        mDevice.wait(Until.findObject(By.text("Static")), TIMEOUT).click();
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+                By.res(SETTINGS_PACKAGE, "ipaddress"));
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+                By.res(SETTINGS_PACKAGE, "gateway"));
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+                By.res(SETTINGS_PACKAGE, "network_prefix_length"));
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+                By.res(SETTINGS_PACKAGE, "dns1"));
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+                By.res(SETTINGS_PACKAGE, "dns2"));
+    }
+
+    @MediumTest
+    public void testPhase2Settings() throws Exception {
+        loadAddNetworkMenu();
+        selectSecurityOption(SECURITY_OPTION_EAP_TEXT);
+
+        BySelector phase2SettingsBySelector =
+                By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_PHASE2_RES_ID).clazz(SPINNER_CLASS);
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR, phase2SettingsBySelector);
+        assertEquals(PHASE2_MENU_NONE_TEXT, mDevice.wait(Until
+                .findObject(phase2SettingsBySelector), TIMEOUT).getChildren().get(0).getText());
+        mDevice.wait(Until.findObject(phase2SettingsBySelector), TIMEOUT).click();
+        Thread.sleep(SLEEP_TIME);
+
+        // Verify Phase 2 authentication spinner options.
+        assertNotNull(mDevice.wait(Until.findObject(By.text(PHASE2_MENU_NONE_TEXT)), TIMEOUT));
+        assertNotNull(mDevice.wait(Until.findObject(By.text(PHASE2_MENU_MSCHAPV2_TEXT)), TIMEOUT));
+        assertNotNull(mDevice.wait(Until.findObject(By.text(PHASE2_MENU_GTC_TEXT)), TIMEOUT));
+    }
+
+    @MediumTest
+    public void testCaCertSettings() throws Exception {
+        loadAddNetworkMenu();
+        selectSecurityOption(SECURITY_OPTION_EAP_TEXT);
+
+        BySelector caCertSettingsBySelector =
+                By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_CACERT_RES_ID).clazz(SPINNER_CLASS);
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR, caCertSettingsBySelector);
+        assertEquals(CACERT_MENU_PLEASE_SELECT_TEXT, mDevice.wait(Until
+                .findObject(caCertSettingsBySelector), TIMEOUT).getChildren().get(0).getText());
+        mDevice.wait(Until.findObject(caCertSettingsBySelector), TIMEOUT).click();
+        Thread.sleep(SLEEP_TIME);
+
+        // Verify CA certificate spinner options.
+        assertNotNull(mDevice.wait(Until.findObject(
+                By.text(CACERT_MENU_PLEASE_SELECT_TEXT)), TIMEOUT));
+        assertNotNull(mDevice.wait(Until.findObject(
+                By.text(CACERT_MENU_USE_SYSTEM_CERTS_TEXT)), TIMEOUT));
+        assertNotNull(mDevice.wait(Until.findObject(
+                By.text(CACERT_MENU_DO_NOT_VALIDATE_TEXT)), TIMEOUT));
+
+        // Verify that a domain field and warning appear when the user selects the
+        // "Use system certificates" option.
+        mDevice.wait(Until.findObject(By.text(CACERT_MENU_USE_SYSTEM_CERTS_TEXT)), TIMEOUT).click();
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+                By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_DOMAIN_LAYOUT_RES_ID));
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+                By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_NO_DOMAIN_WARNING_RES_ID));
+
+        // Verify that a warning appears when the user chooses the "Do Not Validate" option.
+        mDevice.wait(Until.findObject(caCertSettingsBySelector), TIMEOUT).click();
+        mDevice.wait(Until.findObject(By.text(CACERT_MENU_DO_NOT_VALIDATE_TEXT)), TIMEOUT).click();
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+                By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_NO_CACERT_WARNING_RES_ID));
+    }
+
+    @MediumTest
+    public void testAddNetwork_NoSecurity() throws Exception {
+        loadAddNetworkMenu();
+        selectSecurityOption(SECURITY_OPTION_NONE_TEXT);
+
+        // Entering an SSID is enough to enable the submit button. // TODO THIS GUY
+        enterSSID(TEST_SSID);
+        assertTrue(mDevice.wait(Until
+                .findObject(By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+    }
+
+    @MediumTest
+    public void testAddNetwork_WEP() throws Exception {
+        loadAddNetworkMenu();
+        selectSecurityOption(SECURITY_OPTION_WEP_TEXT);
+        assertFalse(mDevice.wait(Until.findObject(
+                By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+
+        // Verify that WEP fields appear.
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+                By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_PASSWORD_LAYOUT_RES_ID));
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+                By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_SHOW_PASSWORD_LAYOUT_RES_ID));
+
+        // Entering an SSID alone does not enable the submit button.
+        enterSSID(TEST_SSID);
+        assertFalse(mDevice.wait(Until.findObject(
+                By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+
+        // Submit button is only enabled after a password is entered.
+        enterPassword(TEST_PW_GE_8_CHAR);
+        assertTrue(mDevice.wait(Until
+                .findObject(By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+    }
+
+    @MediumTest
+    public void testAddNetwork_PSK() throws Exception {
+        loadAddNetworkMenu();
+        selectSecurityOption(SECURITY_OPTION_PSK_TEXT);
+        assertFalse(mDevice.wait(Until.findObject(
+                By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+
+        // Verify that PSK fields appear.
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+                By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_PASSWORD_LAYOUT_RES_ID));
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+                By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_SHOW_PASSWORD_LAYOUT_RES_ID));
+
+        // Entering an SSID alone does not enable the submit button.
+        enterSSID(TEST_SSID);
+        assertFalse(mDevice.wait(Until.findObject(
+                By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+
+        // Entering an password that is too short does not enable submit button.
+        enterPassword(TEST_PW_LT_8_CHAR);
+        assertFalse(mDevice.wait(Until.findObject(
+                By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+
+        // Submit button is only enabled after a password of valid length is entered.
+        enterPassword(TEST_PW_GE_8_CHAR);
+        assertTrue(mDevice.wait(Until.findObject(
+                By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+    }
+
+    @MediumTest
+    public void testAddNetwork_EAP_PEAP() throws Exception {
+        loadAddNetworkMenu();
+        selectSecurityOption(SECURITY_OPTION_EAP_TEXT);
+        assertFalse(mDevice.wait(Until.findObject(
+                By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+
+        selectEAPMethod(EAP_METHOD_PEAP_TEXT);
+
+        // Verify that EAP-PEAP fields appear.
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+                By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_PHASE2_RES_ID));
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+                By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_CACERT_RES_ID));
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+                By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_IDENTITY_LAYOUT_RES_ID));
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+                By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_ANONYMOUS_LAYOUT_RES_ID));
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+                By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_PASSWORD_LAYOUT_RES_ID));
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+                By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_SHOW_PASSWORD_LAYOUT_RES_ID));
+
+        // Entering an SSID alone does not enable the submit button.
+        enterSSID(TEST_SSID);
+        assertFalse(mDevice.wait(Until.findObject(
+                By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+
+        verifyCaCertificateSubmitConditions();
+    }
+
+    @MediumTest
+    public void testAddNetwork_EAP_TLS() throws Exception {
+        loadAddNetworkMenu();
+        selectSecurityOption(SECURITY_OPTION_EAP_TEXT);
+        assertFalse(mDevice.wait(Until.findObject(
+                By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+
+        selectEAPMethod(EAP_METHOD_TLS_TEXT);
+
+        // Verify that EAP-TLS fields appear.
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+                By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_CACERT_RES_ID));
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+                By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_USERCERT_RES_ID));
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+                By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_IDENTITY_LAYOUT_RES_ID));
+
+        // Entering an SSID alone does not enable the submit button.
+        enterSSID(TEST_SSID);
+        assertFalse(mDevice.wait(Until.findObject(
+                By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+
+        // Selecting the User certificate "Do not provide" option alone does not enable the submit
+        // button.
+        selectUserCertificateOption(USERCERT_MENU_DO_NOT_PROVIDE_TEXT);
+        assertFalse(mDevice.wait(Until.findObject(
+                By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+
+        verifyCaCertificateSubmitConditions();
+    }
+
+    @MediumTest
+    public void testAddNetwork_EAP_TTLS() throws Exception {
+        loadAddNetworkMenu();
+        selectSecurityOption(SECURITY_OPTION_EAP_TEXT);
+        assertFalse(mDevice.wait(Until.findObject(
+                By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+
+        selectEAPMethod(EAP_METHOD_TTLS_TEXT);
+
+        // Verify that EAP-TLS fields appear.
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+                By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_PHASE2_RES_ID));
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+                By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_CACERT_RES_ID));
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+                By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_IDENTITY_LAYOUT_RES_ID));
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+                By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_ANONYMOUS_LAYOUT_RES_ID));
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+                By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_PASSWORD_LAYOUT_RES_ID));
+
+        // Entering an SSID alone does not enable the submit button.
+        enterSSID(TEST_SSID);
+        assertFalse(mDevice.wait(Until.findObject(
+                By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+
+        verifyCaCertificateSubmitConditions();
+    }
+
+    @MediumTest
+    public void testAddNetwork_EAP_PWD() throws Exception {
+        loadAddNetworkMenu();
+        selectSecurityOption(SECURITY_OPTION_EAP_TEXT);
+        assertFalse(mDevice.wait(Until.findObject(
+                By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+
+        selectEAPMethod(EAP_METHOD_PWD_TEXT);
+
+        // Verify that EAP-TLS fields appear.
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+                By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_IDENTITY_LAYOUT_RES_ID));
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+                By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_PASSWORD_LAYOUT_RES_ID));
+
+        // Entering an SSID alone enables the submit button.
+        enterSSID(TEST_SSID);
+        assertTrue(mDevice.wait(Until.findObject(
+                By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+    }
+
+    @MediumTest
+    public void testAddNetwork_EAP_SIM() throws Exception {
+        loadAddNetworkMenu();
+        selectSecurityOption(SECURITY_OPTION_EAP_TEXT);
+        assertFalse(mDevice.wait(Until.findObject(
+                By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+
+        selectEAPMethod(EAP_METHOD_SIM_TEXT);
+
+        // Entering an SSID alone enables the submit button.
+        enterSSID(TEST_SSID);
+        assertTrue(mDevice.wait(Until.findObject(
+                By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+    }
+
+    @MediumTest
+    public void testAddNetwork_EAP_AKA() throws Exception {
+        loadAddNetworkMenu();
+        selectSecurityOption(SECURITY_OPTION_EAP_TEXT);
+        assertFalse(mDevice.wait(Until.findObject(
+                By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+
+        selectEAPMethod(EAP_METHOD_AKA_TEXT);
+
+        // Entering an SSID alone enables the submit button.
+        enterSSID(TEST_SSID);
+        assertTrue(mDevice.wait(Until.findObject(
+                By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+    }
+
+    @MediumTest
+    public void testAddNetwork_EAP_AKA_PRIME() throws Exception {
+        loadAddNetworkMenu();
+        selectSecurityOption(SECURITY_OPTION_EAP_TEXT);
+        assertFalse(mDevice.wait(Until.findObject(
+                By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+
+        selectEAPMethod(EAP_METHOD_AKA_PRIME_TEXT);
+
+        // Entering an SSID alone enables the submit button.
+        enterSSID(TEST_SSID);
+        assertTrue(mDevice.wait(Until.findObject(
+                By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+    }
+
+    private void verifyKeepWiFiOnDuringSleep(String settingToBeVerified, int settingValue)
+            throws Exception {
+        loadWiFiConfigureMenu();
+        mDevice.wait(Until.findObject(By.text("Keep Wi‑Fi on during sleep")), TIMEOUT)
+                .click();
+        mDevice.wait(Until.findObject(By.clazz("android.widget.CheckedTextView")
+                .text(settingToBeVerified)), TIMEOUT).click();
+        Thread.sleep(SLEEP_TIME);
+        int keepWiFiOnSetting =
+                Settings.Global.getInt(getInstrumentation().getContext().getContentResolver(),
+                Settings.Global.WIFI_SLEEP_POLICY);
+        assertEquals(settingValue, keepWiFiOnSetting);
+    }
+
+    private void verifyNetworkNotificationsOnOrOff(boolean verifyOn)
+            throws Exception {
+        String switchText = "ON";
+        if (verifyOn) {
+            switchText = "OFF";
+            Settings.Global.putString(getInstrumentation().getContext().getContentResolver(),
+                    Settings.Global.WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON, "0");
+        }
+        else {
+            Settings.Global.putString(getInstrumentation().getContext().getContentResolver(),
+                    Settings.Global.WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON, "1");
+        }
+        loadWiFiConfigureMenu();
+        mDevice.wait(Until.findObject(By.res("android:id/switch_widget").text(switchText)), TIMEOUT)
+                .click();
+        Thread.sleep(SLEEP_TIME);
+        String wifiNotificationValue =
+                Settings.Global.getString(getInstrumentation().getContext().getContentResolver(),
+                Settings.Global.WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON);
+        if (verifyOn) {
+            assertEquals("1", wifiNotificationValue);
+        }
+        else {
+            assertEquals("0", wifiNotificationValue);
+        }
+    }
+
+    private void verifyWiFiOnOrOff(boolean verifyOn) throws Exception {
+         String switchText = "On";
+         if (verifyOn) {
+             switchText = "Off";
+         }
+         loadWiFiSettingsPage(!verifyOn);
+         mDevice.wait(Until
+                 .findObject(By.res(SETTINGS_PACKAGE, "switch_bar").text(switchText)), TIMEOUT)
+                 .click();
+         Thread.sleep(SLEEP_TIME);
+         String wifiValue =
+                 Settings.Global.getString(getInstrumentation().getContext().getContentResolver(),
+                 Settings.Global.WIFI_ON);
+         if (verifyOn) {
+             assertEquals("1", wifiValue);
+         }
+         else {
+             assertEquals("0", wifiValue);
+         }
+    }
+
+    private void verifyCaCertificateSubmitConditions() throws Exception {
+        // Selecting the CA certificate "Do not validate" option enables the submit button.
+        selectCaCertificateOption(CACERT_MENU_DO_NOT_VALIDATE_TEXT);
+        assertTrue(mDevice.wait(Until.findObject(
+                By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+
+        // However, selecting the CA certificate "Use system certificates option" is not enough to
+        // enable the submit button.
+        selectCaCertificateOption(CACERT_MENU_USE_SYSTEM_CERTS_TEXT);
+        assertFalse(mDevice.wait(Until.findObject(
+                By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+
+        // Submit button is only enabled after a domain is entered as well.
+        enterDomain(TEST_DOMAIN);
+        assertTrue(mDevice.wait(Until.findObject(
+                By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+    }
+
+    private void loadWiFiSettingsPage(boolean wifiEnabled) throws Exception {
+        WifiManager wifiManager = (WifiManager)getInstrumentation().getContext()
+                .getSystemService(Context.WIFI_SERVICE);
+        wifiManager.setWifiEnabled(wifiEnabled);
+        SettingsHelper.launchSettingsPage(getInstrumentation().getContext(),
+                Settings.ACTION_WIFI_SETTINGS);
+    }
+
+    private void loadWiFiConfigureMenu() throws Exception {
+        loadWiFiSettingsPage(true);
+        mDevice.wait(Until.findObject(By.desc("Configure")), TIMEOUT).click();
+    }
+
+    private void loadAddNetworkMenu() throws Exception {
+        loadWiFiSettingsPage(true);
+        for (int attempts = 0; attempts < MAX_ADD_NETWORK_BUTTON_ATTEMPTS; ++attempts) {
+            UiObject2 found = null;
+            try {
+                findOrScrollToObject(By.scrollable(true), By.text(ADD_NETWORK_PREFERENCE_TEXT))
+                        .click();
+            } catch (StaleObjectException e) {
+                // The network list might have been updated between when the Add network button was
+                // found, and when it UI automator attempted to click on it. Retry.
+                continue;
+            }
+            // If we get here, we successfully clicked on the Add network button, so we are done.
+            // Adding a sleep and a back press to dismiss the IME, as a workaround for
+            // b/28862652
+            Thread.sleep(SLEEP_TIME*5);
+            mDevice.pressBack();
+            return;
+        }
+
+        fail("Failed to load Add Network Menu after " + MAX_ADD_NETWORK_BUTTON_ATTEMPTS
+                + " retries");
+    }
+
+    private void selectSecurityOption(String securityOption) throws Exception {
+        // We might not need to scroll to the security options if not enough add network menu
+        // options are visible.
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+                By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_SECURITY_OPTION_RES_ID)
+                .clazz(SPINNER_CLASS)).click();
+        Thread.sleep(SLEEP_TIME);
+        mDevice.wait(Until.findObject(By.text(securityOption)), TIMEOUT).click();
+    }
+
+    private void selectEAPMethod(String eapMethod) throws Exception {
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+                By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_EAP_METHOD_RES_ID).clazz(SPINNER_CLASS))
+                .click();
+        Thread.sleep(SLEEP_TIME);
+        findOrScrollToObject(SPINNER_OPTIONS_SCROLLABLE_BY_SELECTOR, By.text(eapMethod)).click();
+    }
+
+    private void selectUserCertificateOption(String userCertificateOption) throws Exception {
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+                By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_USERCERT_RES_ID).clazz(SPINNER_CLASS))
+                .click();
+        mDevice.wait(Until.findObject(By.text(userCertificateOption)), TIMEOUT).click();
+    }
+
+    private void selectCaCertificateOption(String caCertificateOption) throws Exception {
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+                By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_CACERT_RES_ID).clazz(SPINNER_CLASS))
+                .click();
+        mDevice.wait(Until.findObject(By.text(caCertificateOption)), TIMEOUT).click();
+    }
+
+    private void enterSSID(String ssid) throws Exception {
+        // We might not need to scroll to the SSID option if not enough add network menu options
+        // are visible.
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+                By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_SSID_RES_ID).clazz(EDIT_TEXT_CLASS))
+                .setText(ssid);
+    }
+
+    private void enterPassword(String password) throws Exception {
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+                By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_PASSWORD_RES_ID).clazz(EDIT_TEXT_CLASS))
+                .setText(password);
+    }
+
+    private void enterDomain(String domain) throws Exception {
+        findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+                By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_DOMAIN_RES_ID)).setText(domain);
+    }
+
+    // Use this if the UI object might or might not need to be scrolled to.
+    private UiObject2 findOrScrollToObject(BySelector scrollableSelector, BySelector objectSelector)
+            throws Exception {
+        UiObject2 object = mDevice.wait(Until.findObject(objectSelector), TIMEOUT);
+        if (object == null) {
+            object = scrollToObject(scrollableSelector, objectSelector);
+        }
+        return object;
+    }
+
+    private UiObject2 scrollToObject(BySelector scrollableSelector, BySelector objectSelector)
+            throws Exception {
+        UiObject2 scrollable = mDevice.wait(Until.findObject(scrollableSelector), TIMEOUT);
+        if (scrollable == null) {
+            fail("Could not find scrollable UI object identified by " + scrollableSelector);
+        }
+        UiObject2 found = null;
+        // Scroll all the way up first, then all the way down.
+        while (true) {
+            // Optimization: terminate if we find the object while scrolling up to reset, so
+            // we save the time spent scrolling down again.
+            boolean canScrollAgain = scrollable.scroll(Direction.UP, SCROLL_UP_PERCENT,
+                    SCROLL_SPEED);
+            found = mDevice.findObject(objectSelector);
+            if (found != null) return found;
+            if (!canScrollAgain) break;
+        }
+        for (int attempts = 0; found == null && attempts < MAX_SCROLL_ATTEMPTS; ++attempts) {
+            // Return value of UiObject2.scroll() is not reliable, so do not use it in loop
+            // condition, in case it causes this loop to terminate prematurely.
+            scrollable.scroll(Direction.DOWN, SCROLL_DOWN_PERCENT, SCROLL_SPEED);
+            found = mDevice.findObject(objectSelector);
+        }
+        if (found == null) {
+            fail("Could not scroll to UI object identified by " + objectSelector);
+        }
+        return found;
+    }
+}
diff --git a/tests/functional/testapks/applinktestapp/Android.mk b/tests/functional/testapks/applinktestapp/Android.mk
new file mode 100644
index 0000000..08c75f8
--- /dev/null
+++ b/tests/functional/testapks/applinktestapp/Android.mk
@@ -0,0 +1,26 @@
+# 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
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT 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_TAGS := tests
+
+# omit gradle 'build' dir
+LOCAL_SRC_FILES := $(call all-java-files-under,src)
+
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+LOCAL_PACKAGE_NAME := AppLinkTestApp
+LOCAL_CERTIFICATE := platform
+include $(BUILD_PACKAGE)
diff --git a/tests/functional/testapks/applinktestapp/AndroidManifest.xml b/tests/functional/testapks/applinktestapp/AndroidManifest.xml
new file mode 100644
index 0000000..6c0b4ad
--- /dev/null
+++ b/tests/functional/testapks/applinktestapp/AndroidManifest.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 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.applinktestapp"
+        android:versionCode="1"
+        android:versionName="1.0"
+        android:sharedUserId="com.android.functional.applink" >
+
+    <uses-sdk android:minSdkVersion="19"
+          android:targetSdkVersion="24"/>
+
+    <application
+        android:icon="@mipmap/ic_launcher"
+        android:label="AppLinkTestApp" >
+        <activity android:name=".MainActivity" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+<activity android:name=".MainActivity">
+    <intent-filter>
+        <action android:name="android.intent.action.VIEW" />
+        <category android:name="android.intent.category.DEFAULT" />
+        <category android:name="android.intent.category.BROWSABLE" />
+        <data android:host="youtube.com" />
+        <data android:scheme="http" />
+        <data android:host="test.com" />
+    </intent-filter>
+</activity>
+    </application>
+</manifest>
diff --git a/tests/functional/testapks/applinktestapp/res/layout/activity_main.xml b/tests/functional/testapks/applinktestapp/res/layout/activity_main.xml
new file mode 100644
index 0000000..319faed
--- /dev/null
+++ b/tests/functional/testapks/applinktestapp/res/layout/activity_main.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:layout_width="match_parent"
+              android:layout_height="match_parent"
+              android:orientation="vertical" >
+    <TextView android:id="@+id/text"
+              android:layout_width="wrap_content"
+              android:layout_height="wrap_content"
+              android:text="App Link Test App" />
+</LinearLayout>
diff --git a/tests/functional/testapks/applinktestapp/res/mipmap-hdpi/ic_launcher.png b/tests/functional/testapks/applinktestapp/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..cde69bc
--- /dev/null
+++ b/tests/functional/testapks/applinktestapp/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/tests/functional/testapks/applinktestapp/src/com/android/applinktestapp/MainActivity.java b/tests/functional/testapks/applinktestapp/src/com/android/applinktestapp/MainActivity.java
new file mode 100644
index 0000000..b69c222
--- /dev/null
+++ b/tests/functional/testapks/applinktestapp/src/com/android/applinktestapp/MainActivity.java
@@ -0,0 +1,30 @@
+/*
+ * 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.applinktestapp;
+
+import android.os.Bundle;
+import android.content.Intent;
+import android.app.Activity;
+import android.net.Uri;
+
+public class MainActivity extends Activity {
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_main);
+    }
+}
diff --git a/tests/functional/testapks/permissiontestappmv1/Android.mk b/tests/functional/testapks/permissiontestappmv1/Android.mk
new file mode 100644
index 0000000..0bf4196
--- /dev/null
+++ b/tests/functional/testapks/permissiontestappmv1/Android.mk
@@ -0,0 +1,14 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+
+# omit gradle 'build' dir
+LOCAL_SRC_FILES := $(call all-java-files-under,src)
+
+LOCAL_STATIC_JAVA_LIBRARIES = android-support-v4
+LOCAL_RESOURCE_DIR := \
+    $(LOCAL_PATH)/res
+LOCAL_PACKAGE_NAME := PermissionTestAppMV1
+LOCAL_CERTIFICATE := platform
+include $(BUILD_PACKAGE)
diff --git a/tests/functional/testapks/permissiontestappmv1/AndroidManifest.xml b/tests/functional/testapks/permissiontestappmv1/AndroidManifest.xml
new file mode 100644
index 0000000..cf6d152
--- /dev/null
+++ b/tests/functional/testapks/permissiontestappmv1/AndroidManifest.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.permissiontestappmv1"
+        android:versionCode="1"
+        android:versionName="1.0" >
+
+    <uses-sdk android:minSdkVersion="23"
+          android:targetSdkVersion="23"/>
+
+    <uses-permission android:name="android.permission.READ_CONTACTS" />
+    <application
+        android:icon="@mipmap/ic_launcher"
+        android:label="PermissionTestAppMV1" >
+        <activity android:name=".MainActivity" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+
+</manifest>
diff --git a/tests/functional/testapks/permissiontestappmv1/res/layout/activity_main.xml b/tests/functional/testapks/permissiontestappmv1/res/layout/activity_main.xml
new file mode 100644
index 0000000..a298f09
--- /dev/null
+++ b/tests/functional/testapks/permissiontestappmv1/res/layout/activity_main.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:layout_width="match_parent"
+              android:layout_height="match_parent"
+              android:orientation="vertical" >
+    <TextView android:id="@+id/text"
+              android:layout_width="wrap_content"
+              android:layout_height="wrap_content"
+              android:text="Build Target: M\nVersion: 1.0\nDangerous Permissions:Contacts\nNormal Permission: \n\n\n" />
+    <Button android:id="@+id/buttonGetPermission"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="Get Contact Permission" />
+</LinearLayout>
diff --git a/tests/functional/testapks/permissiontestappmv1/res/mipmap-hdpi/ic_launcher.png b/tests/functional/testapks/permissiontestappmv1/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..cde69bc
--- /dev/null
+++ b/tests/functional/testapks/permissiontestappmv1/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/tests/functional/testapks/permissiontestappmv1/src/com/android/permissontestappmv1/MainActivity.java b/tests/functional/testapks/permissiontestappmv1/src/com/android/permissontestappmv1/MainActivity.java
new file mode 100644
index 0000000..a6cd8a8
--- /dev/null
+++ b/tests/functional/testapks/permissiontestappmv1/src/com/android/permissontestappmv1/MainActivity.java
@@ -0,0 +1,68 @@
+/*
+ * 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.permissiontestappmv1;
+
+import android.provider.Settings;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.content.ContextCompat;
+import android.os.Bundle;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.Button;
+import android.view.View;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.widget.EditText;
+import android.widget.Toast;
+import android.view.inputmethod.InputMethodManager;
+import android.content.ContentResolver;
+import android.Manifest;
+import android.app.Activity;
+
+public class MainActivity extends Activity {
+
+    private static final int READ_PERMISSION_RESULT = 1;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_main);
+
+        final Button buttonGetPermission = (Button) findViewById(R.id.buttonGetPermission);
+        buttonGetPermission.setOnClickListener(new View.OnClickListener() {
+            public void onClick(View v) {
+                showContacts();
+            }
+        });
+    }
+
+    /**
+      * This method asks for 'READ_CONTACTS' permission on button 'Get Contact Permission' press
+      * Method does nothing with Contact content
+      */
+    private void showContacts() {
+        if (ContextCompat.checkSelfPermission(this,
+                Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
+            if (ActivityCompat.shouldShowRequestPermissionRationale(this,
+                    Manifest.permission.READ_CONTACTS)) {
+            }
+            ActivityCompat.requestPermissions(this, new String[] {
+                    Manifest.permission.READ_CONTACTS
+            }, READ_PERMISSION_RESULT);
+            return;
+        }
+    }
+}
diff --git a/tests/jank/UbSystemUiJankTests/src/android/platform/systemui/tests/jank/SettingsJankTests.java b/tests/jank/UbSystemUiJankTests/src/android/platform/systemui/tests/jank/SettingsJankTests.java
index b783ce6..e6c7b8e 100644
--- a/tests/jank/UbSystemUiJankTests/src/android/platform/systemui/tests/jank/SettingsJankTests.java
+++ b/tests/jank/UbSystemUiJankTests/src/android/platform/systemui/tests/jank/SettingsJankTests.java
@@ -41,7 +41,8 @@
 
     private static final int TIMEOUT = 5000;
     private static final String SETTINGS_PACKAGE = "com.android.settings";
-    private static final BySelector SETTINGS_DASHBOARD = By.res(SETTINGS_PACKAGE, "dashboard");
+    private static final BySelector SETTINGS_DASHBOARD = By.res(SETTINGS_PACKAGE,
+            "dashboard_container");
     // 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 = 2;
@@ -70,7 +71,6 @@
         intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); // Clear out any previous instances
         context.startActivity(intent);
         mDevice.wait(Until.hasObject(By.pkg(SETTINGS_PACKAGE).depth(0)), TIMEOUT);
-
         SystemClock.sleep(1000);
     }
 
@@ -82,6 +82,12 @@
 
     public void flingSettingsToStart() throws IOException {
         UiObject2 list = mDevice.wait(Until.findObject(SETTINGS_DASHBOARD), TIMEOUT);
+        int count = 0;
+        while (!list.isScrollable() && count <= 5) {
+            mDevice.wait(Until.findObject(By.text("SEE ALL")), TIMEOUT).click();
+            list = mDevice.wait(Until.findObject(SETTINGS_DASHBOARD), TIMEOUT);
+            count++;
+        }
         while (list.fling(Direction.UP));
         mDevice.waitForIdle();
         TimeResultLogger.writeTimeStampLogStart(String.format("%s-%s",
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 789461a..94599f9 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
@@ -116,8 +116,8 @@
         }
 
         // Close any crash dialogs
-        while (mDevice.hasObject(By.textContains("has stopped."))) {
-            mDevice.findObject(By.text("OK")).clickAndWait(Until.newWindow(), 2000);
+        while (mDevice.hasObject(By.textContains("has stopped"))) {
+            mDevice.findObject(By.text("Close")).clickAndWait(Until.newWindow(), 2000);
         }
         TimeResultLogger.writeTimeStampLogStart(String.format("%s-%s",
                 getClass().getSimpleName(), getName()), TIMESTAMP_FILE);
diff --git a/tests/jank/androidtvjanktests/Android.mk b/tests/jank/androidtvjanktests/Android.mk
new file mode 100644
index 0000000..f01074f
--- /dev/null
+++ b/tests/jank/androidtvjanktests/Android.mk
@@ -0,0 +1,26 @@
+# 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.
+
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_PACKAGE_NAME := AndroidTVJankTests
+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_SDK_VERSION := 21
+
+include $(BUILD_PACKAGE)
\ No newline at end of file
diff --git a/tests/jank/androidtvjanktests/AndroidManifest.xml b/tests/jank/androidtvjanktests/AndroidManifest.xml
new file mode 100644
index 0000000..bdd3039
--- /dev/null
+++ b/tests/jank/androidtvjanktests/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.androidtv.janktests">
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <uses-sdk android:minSdkVersion="19"
+          android:targetSdkVersion="23"/>
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <instrumentation
+            android:name="android.test.InstrumentationTestRunner"
+            android:targetPackage="com.android.androidtv.janktests"
+            android:label="Platform Android TV Jank Tests" />
+</manifest>
diff --git a/tests/jank/androidtvjanktests/src/com/android/androidtv/janktests/SystemAppJankTests.java b/tests/jank/androidtvjanktests/src/com/android/androidtv/janktests/SystemAppJankTests.java
new file mode 100644
index 0000000..ccd913c
--- /dev/null
+++ b/tests/jank/androidtvjanktests/src/com/android/androidtv/janktests/SystemAppJankTests.java
@@ -0,0 +1,118 @@
+/*
+ * 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 com.android.androidtv.janktests;
+
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.os.SystemClock;
+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.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;
+
+/*
+ * This class contains the tests for key system apps on Android TV jank.
+ */
+public class SystemAppJankTests extends JankTestBase {
+
+    private static final int LONG_TIMEOUT = 5000;
+    private static final int INNER_LOOP = 8;
+    private static final int FLING_SPEED = 12000;
+    private static final String YOUTUBE_PACKAGE = "com.google.android.youtube.tv";
+    private UiDevice mDevice;
+
+    @Override
+    public void setUp() {
+        mDevice = UiDevice.getInstance(getInstrumentation());
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    public void afterTestSystemApp(Bundle metrics) throws IOException {
+        mDevice.pressHome();
+        super.afterTest(metrics);
+    }
+
+    public void launchYoutube() throws UiObjectNotFoundException {
+        mDevice.pressHome();
+        launchApp(YOUTUBE_PACKAGE);
+        SystemClock.sleep(LONG_TIMEOUT);
+        // Ensure that Youtube has loaded on Android TV with nav bar in focus
+        UiObject2 youtubeScreen = mDevice.wait(
+                Until.findObject(By.scrollable(true).res(YOUTUBE_PACKAGE, "guide")), LONG_TIMEOUT);
+    }
+
+    // Measures jank while scrolling down the Youtube Navigation Bar
+    @JankTest(expectedFrames=100, beforeTest = "launchYoutube",
+            afterTest="afterTestSystemApp")
+    @GfxMonitor(processName=YOUTUBE_PACKAGE)
+    public void testYoutubeGuideNavigation() throws UiObjectNotFoundException {
+        // As of launching Youtube, we're already at the screen where
+        // the navigation bar is in focus, so we only need to scroll.
+        navigateDownAndUpCurrentScreen();
+    }
+
+    public void goToYoutubeContainer() throws UiObjectNotFoundException {
+        launchYoutube();
+        // Move focus from Youtube navigation bar to content
+        mDevice.pressDPadRight();
+        SystemClock.sleep(LONG_TIMEOUT);
+        // Ensure that Youtube content is in focus
+        UiObject2 youtubeScreen = mDevice.wait( Until.findObject(By.scrollable(true)
+                .res(YOUTUBE_PACKAGE, "container_list")), LONG_TIMEOUT);
+    }
+
+    // Measures jank while scrolling down the Youtube Navigation Bar
+    @JankTest(expectedFrames=100, beforeTest = "goToYoutubeContainer",
+            afterTest="afterTestSystemApp")
+    @GfxMonitor(processName=YOUTUBE_PACKAGE)
+    public void testYoutubeContainerListNavigation() throws UiObjectNotFoundException {
+        // The gotoYouTubeContainer method confirms that the focus is
+        // on the content, so we only need to scroll.
+        navigateDownAndUpCurrentScreen();
+    }
+
+    public void navigateDownAndUpCurrentScreen() {
+        for (int i = 0; i < INNER_LOOP; i++) {
+            // Press DPad button down eight times in succession to scroll down.
+            mDevice.pressDPadDown();
+        }
+        for (int i = 0; i < INNER_LOOP; i++) {
+            // Press DPad button up eight times in succession to scroll up.
+            mDevice.pressDPadUp();
+        }
+    }
+
+    public void launchApp(String packageName) throws UiObjectNotFoundException {
+        PackageManager pm = getInstrumentation().getContext().getPackageManager();
+        Intent appIntent = pm.getLaunchIntentForPackage(packageName);
+        appIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        getInstrumentation().getContext().startActivity(appIntent);
+        SystemClock.sleep(LONG_TIMEOUT);
+    }
+}
\ No newline at end of file
diff --git a/tests/jank/androidtvjanktests/src/com/android/androidtv/janktests/SystemUiJankTests.java b/tests/jank/androidtvjanktests/src/com/android/androidtv/janktests/SystemUiJankTests.java
new file mode 100644
index 0000000..444c19b
--- /dev/null
+++ b/tests/jank/androidtvjanktests/src/com/android/androidtv/janktests/SystemUiJankTests.java
@@ -0,0 +1,143 @@
+/*
+ * 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 com.android.androidtv.janktests;
+
+import android.os.Bundle;
+import android.os.SystemClock;
+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.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;
+
+/*
+ * This class contains the tests for Android TV jank.
+ */
+public class SystemUiJankTests extends JankTestBase {
+
+    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 String SETTINGS_PACKAGE = "com.android.tv.settings";
+    private UiDevice mDevice;
+
+    @Override
+    public void setUp() {
+        mDevice = UiDevice.getInstance(getInstrumentation());
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    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")),
+                SHORT_TIMEOUT);
+    }
+
+    public void afterTestHomeScreenNavigation(Bundle metrics) throws IOException {
+        super.afterTest(metrics);
+    }
+
+    // Measures jank while scrolling down the Home screen
+    @JankTest(expectedFrames=100, beforeTest = "goHome",
+            afterTest="afterTestHomeScreenNavigation")
+    @GfxMonitor(processName=LEANBACK_LAUNCHER)
+    public void testHomeScreenNavigation() throws UiObjectNotFoundException {
+        // We've already verified that Home screen is being displayed.
+        // Scroll up and down the home screen.
+        navigateDownAndUpCurrentScreen();
+    }
+
+    // Navigates to the Settings row on the Home screen
+    public void goToSettingsRow() {
+        // 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");
+        }
+    }
+
+    public void afterTestSettings(Bundle metrics) throws IOException {
+        // Navigate back home
+        goHome();
+        super.afterTest(metrics);
+    }
+
+    // Measures jank while navigating to Settings from Home and back
+    @JankTest(expectedFrames=100, beforeTest="goToSettingsRow",
+            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();
+            // Press Back button to go back to the Home screen with focus on Settings
+            mDevice.pressBack();
+        }
+    }
+
+    // Navigates to the Settings Screen
+    public void goToSettings() {
+        goToSettingsRow();
+        mDevice.pressDPadCenter();
+    }
+
+    // Measures jank while scrolling on the Settings screen
+    @JankTest(expectedFrames=100, beforeTest="goToSettings",
+            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();
+    }
+
+    public void navigateDownAndUpCurrentScreen() {
+        for (int i = 0; i < INNER_LOOP; i++) {
+            // Press DPad button down eight times in succession
+            mDevice.pressDPadDown();
+        }
+        for (int i = 0; i < INNER_LOOP; i++) {
+            // Press DPad button up eight times in succession.
+            mDevice.pressDPadUp();
+        }
+    }
+}
diff --git a/tests/jank/dialer/AndroidManifest.xml b/tests/jank/dialer/AndroidManifest.xml
index 3973f4f..b59e090 100644
--- a/tests/jank/dialer/AndroidManifest.xml
+++ b/tests/jank/dialer/AndroidManifest.xml
@@ -16,12 +16,12 @@
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.dialer.janktests">
-
     <uses-sdk android:minSdkVersion="22" />
     <uses-permission android:name="android.permission.READ_CONTACTS" />
     <uses-permission android:name="android.permission.WRITE_CONTACTS" />
     <uses-permission android:name="android.permission.READ_CALL_LOG" />
     <uses-permission android:name="android.permission.WRITE_CALL_LOG" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
 
     <application>
         <uses-library android:name="android.test.runner" />
diff --git a/tests/jank/dialer/src/com/android/dialer/janktests/DialerJankTests.java b/tests/jank/dialer/src/com/android/dialer/janktests/DialerJankTests.java
index e252e28..61af03e 100644
--- a/tests/jank/dialer/src/com/android/dialer/janktests/DialerJankTests.java
+++ b/tests/jank/dialer/src/com/android/dialer/janktests/DialerJankTests.java
@@ -97,8 +97,7 @@
         mDevice.waitForIdle();
 
         // Open contacts list
-        UiObject2 contacts = mDevice.wait(Until.findObject(
-                By.clazz(View.class).desc("Contacts")), TIMEOUT);
+        UiObject2 contacts = mDevice.wait(Until.findObject(By.desc("Contacts")), TIMEOUT);
         assertNotNull("Contacts can't be found", contacts);
         contacts.clickAndWait(Until.newWindow(), TIMEOUT);
         // Find a contact by a given contact-name
@@ -141,19 +140,16 @@
         }
         launchApp(PACKAGE_NAME);
         mDevice.waitForIdle();
-        // Find recents and click
-        mDevice.wait(Until.findObject(By.clazz(View.class).desc("Recents")), TIMEOUT).click();
-        // to ensure enough record for fling, expand full call-history
-        mDevice.wait(Until.findObject(
-                By.res(RES_PACKAGE_NAME,"lists_pager")), TIMEOUT).fling(Direction.DOWN);
-        mDevice.wait(Until.findObject(By.text("View full call history")), TIMEOUT).click();
+        // Find 'Call History' and click
+        mDevice.wait(Until.findObject(By.desc("Call History")), TIMEOUT).click();
+        mDevice.wait(Until.findObject(By.res(RES_PACKAGE_NAME,"lists_pager")), TIMEOUT);
     }
 
     @JankTest(beforeTest="launchCallLog", expectedFrames=EXPECTED_FRAMES)
     @GfxMonitor(processName=PACKAGE_NAME)
     public void testDialerCallLogFling() {
         UiObject2 callLog = mDevice.wait(Until.findObject(
-                By.res(RES_PACKAGE_NAME, "call_log_pager")), TIMEOUT);
+                By.res(RES_PACKAGE_NAME,"lists_pager")), TIMEOUT);
         assertNotNull("Call log can't be found", callLog);
         for (int i = 0; i < INNER_LOOP; i++) {
             callLog.fling(Direction.DOWN);
diff --git a/tests/jank/jankmicrobenchmark/Android.mk b/tests/jank/jankmicrobenchmark/Android.mk
index 4819ef2..d0be009 100644
--- a/tests/jank/jankmicrobenchmark/Android.mk
+++ b/tests/jank/jankmicrobenchmark/Android.mk
@@ -21,6 +21,7 @@
 
 LOCAL_STATIC_JAVA_LIBRARIES := ub-uiautomator ub-janktesthelper
 
-LOCAK_SDK_VERSION := current
+LOCAL_SDK_VERSION := current
 
 include $(BUILD_PACKAGE)
+
diff --git a/tests/jank/jankmicrobenchmark/AndroidManifest.xml b/tests/jank/jankmicrobenchmark/AndroidManifest.xml
index 4ce5e54..d856757 100644
--- a/tests/jank/jankmicrobenchmark/AndroidManifest.xml
+++ b/tests/jank/jankmicrobenchmark/AndroidManifest.xml
@@ -16,7 +16,8 @@
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.jankmicrobenchmark.janktests">
-
+    <uses-sdk android:minSdkVersion="23" android:targetSdkVersion="23" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <application>
         <uses-library android:name="android.test.runner" />
     </application>
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 ec2bca5..9b8a9bb 100644
--- a/tests/jank/jankmicrobenchmark/src/com/android/jankmicrobenchmark/janktests/ApiDemoJankTests.java
+++ b/tests/jank/jankmicrobenchmark/src/com/android/jankmicrobenchmark/janktests/ApiDemoJankTests.java
@@ -45,7 +45,9 @@
     private static final int EXPECTED_FRAMES = 100;
     private static final String PACKAGE_NAME = "com.example.android.apis";
     private static final String RES_PACKAGE_NAME = "android";
+    private static final String LEANBACK_LAUNCHER = "com.google.android.leanbacklauncher";
     private UiDevice mDevice;
+    private UiObject2 mListView;
 
     @Override
     public void setUp() throws Exception {
@@ -60,14 +62,32 @@
         super.tearDown();
     }
 
-    public void launchApiDemos() {
+    // This method distinguishes between home screen for handheld devices
+    // and home screen for Android TV, both of whom have different Home elements.
+    public UiObject2 getHomeScreen() throws UiObjectNotFoundException {
+        if (mDevice.getProductName().equals("fugu")) {
+            return mDevice.wait(Until.findObject(By.res(LEANBACK_LAUNCHER, "main_list_view")),
+                    LONG_TIMEOUT);
+        }
+        else {
+            String launcherPackage = mDevice.getLauncherPackageName();
+            return mDevice.wait(Until.findObject(By.res(launcherPackage,"workspace")),
+                    LONG_TIMEOUT);
+        }
+    }
+
+    public void launchApiDemos() throws UiObjectNotFoundException {
+        UiObject2 homeScreen = getHomeScreen();
+        if (homeScreen == null)
+            navigateToHome();
         Intent intent = getInstrumentation().getContext().getPackageManager()
                 .getLaunchIntentForPackage(PACKAGE_NAME);
         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         getInstrumentation().getContext().startActivity(intent);
         mDevice.waitForIdle();
     }
-    public void selectAnimation(String optionName) {
+
+    public void selectAnimation(String optionName) throws UiObjectNotFoundException {
         launchApiDemos();
         UiObject2 animation = mDevice.wait(Until.findObject(
                 By.res(RES_PACKAGE_NAME, "text1").text("Animation")), LONG_TIMEOUT);
@@ -78,7 +98,7 @@
         int maxAttempt = 3;
         while (option == null && maxAttempt > 0) {
             mDevice.wait(Until.findObject(By.res(RES_PACKAGE_NAME, "content")), LONG_TIMEOUT)
-                    .scroll(Direction.DOWN, 1.0f);
+            .scroll(Direction.DOWN, 1.0f);
             option = mDevice.wait(Until.findObject(By.res(RES_PACKAGE_NAME, "text1")
                     .text(optionName)), LONG_TIMEOUT);
             --maxAttempt;
@@ -87,31 +107,41 @@
         option.click();
     }
 
+    // Since afterTest only runs when the test has passed, there's no way of going
+    // back to the Home Screen if a test fails. This method is a workaround. A feature
+    // request has been filed to have a per test tearDown method - b/25673300
+    public void navigateToHome() throws UiObjectNotFoundException {
+        UiObject2 homeScreen = getHomeScreen();
+        int count = 0;
+        while (homeScreen == null && count <= 10) {
+            mDevice.pressBack();
+            homeScreen = getHomeScreen();
+            count++;
+        }
+        Assert.assertNotNull("Hit maximum retries and couldn't find Home Screen", homeScreen);
+    }
+
     // Since the app doesn't start at the first page when reloaded after the first time,
     // ensuring that we head back to the first screen before going Home so we're always
     // on screen one.
     public void goBackHome(Bundle metrics) throws UiObjectNotFoundException {
-        String launcherPackage = mDevice.getLauncherPackageName();
-        UiObject2 homeScreen = mDevice.findObject(By.res(launcherPackage,"workspace"));
-        while (homeScreen == null) {
-            mDevice.pressBack();
-            homeScreen = mDevice.findObject(By.res(launcherPackage,"workspace"));
-        }
+        navigateToHome();
         super.afterTest(metrics);
     }
 
     // Loads the 'activity transition' animation
     public void selectActivityTransitionAnimation() throws UiObjectNotFoundException {
-         selectAnimation("Activity Transition");
+        selectAnimation("Activity Transition");
     }
 
     // Measures jank for activity transition animation
     @JankTest(beforeTest="selectActivityTransitionAnimation", afterTest="goBackHome",
-        expectedFrames=EXPECTED_FRAMES)
+            expectedFrames=EXPECTED_FRAMES)
     @GfxMonitor(processName=PACKAGE_NAME)
     public void testActivityTransitionAnimation() {
         for (int i = 0; i < INNER_LOOP; i++) {
-            UiObject2 redBallTile = mDevice.findObject(By.res(PACKAGE_NAME, "ball"));
+            UiObject2 redBallTile = mDevice.wait(Until.findObject(By.res(PACKAGE_NAME, "ball")),
+                    LONG_TIMEOUT);
             redBallTile.click();
             SystemClock.sleep(LONG_TIMEOUT);
             mDevice.pressBack();
@@ -125,7 +155,7 @@
 
     // Measures jank for view flip animation
     @JankTest(beforeTest="selectViewFlipAnimation", afterTest="goBackHome",
-        expectedFrames=EXPECTED_FRAMES)
+            expectedFrames=EXPECTED_FRAMES)
     @GfxMonitor(processName=PACKAGE_NAME)
     public void testViewFlipAnimation() {
         for (int i = 0; i < INNER_LOOP; i++) {
@@ -142,7 +172,7 @@
 
     // Measures jank for cloning animation
     @JankTest(beforeTest="selectCloningAnimation", afterTest="goBackHome",
-        expectedFrames=EXPECTED_FRAMES)
+            expectedFrames=EXPECTED_FRAMES)
     @GfxMonitor(processName=PACKAGE_NAME)
     public void testCloningAnimation() {
         for (int i = 0; i < INNER_LOOP; i++) {
@@ -159,11 +189,11 @@
 
     // Measures jank for 'loading' animation
     @JankTest(beforeTest="selectLoadingOption", afterTest="goBackHome",
-              expectedFrames=EXPECTED_FRAMES)
+            expectedFrames=EXPECTED_FRAMES)
     @GfxMonitor(processName=PACKAGE_NAME)
     public void testLoadingJank() {
         UiObject2 runButton = mDevice.wait(Until.findObject(
-            By.res(PACKAGE_NAME, "startButton").text("Run")), LONG_TIMEOUT);
+                By.res(PACKAGE_NAME, "startButton").text("RUN")), LONG_TIMEOUT);
         Assert.assertNotNull("Run button is null", runButton);
         for (int i = 0; i < INNER_LOOP; i++) {
             runButton.click();
@@ -178,7 +208,7 @@
 
     // Measures jank for 'simple transition' animation
     @JankTest(beforeTest="selectSimpleTransitionOption", afterTest="goBackHome",
-              expectedFrames=EXPECTED_FRAMES)
+            expectedFrames=EXPECTED_FRAMES)
     @GfxMonitor(processName=PACKAGE_NAME)
     public void testSimpleTransitionJank() {
         for (int i = 0; i < INNER_LOOP; i++) {
@@ -203,12 +233,12 @@
 
     // Measures jank for 'hide/show' animation
     @JankTest(beforeTest="selectHideShowAnimationOption", afterTest="goBackHome",
-              expectedFrames=EXPECTED_FRAMES)
+            expectedFrames=EXPECTED_FRAMES)
     @GfxMonitor(processName=PACKAGE_NAME)
     public void testHideShowAnimationJank() {
         for (int i = 0; i < INNER_LOOP; i++) {
             UiObject2 showButton = mDevice.wait(Until.findObject(By.res(
-                    PACKAGE_NAME, "addNewButton").text("Show Buttons")), LONG_TIMEOUT);
+                    PACKAGE_NAME, "addNewButton").text("SHOW BUTTONS")), LONG_TIMEOUT);
             Assert.assertNotNull("'Show Buttons' button can't be found", showButton);
             showButton.click();
             SystemClock.sleep(SHORT_TIMEOUT);
@@ -239,7 +269,7 @@
         }
     }
 
-    public void selectViews(String optionName) {
+    public void selectViews(String optionName) throws UiObjectNotFoundException {
         launchApiDemos();
         UiObject2 views = null;
         short maxAttempt = 4;
@@ -248,7 +278,7 @@
                     .text("Views")), LONG_TIMEOUT);
             if (views == null) {
                 mDevice.wait(Until.findObject(By.res(RES_PACKAGE_NAME, "content")), LONG_TIMEOUT)
-                        .scroll(Direction.DOWN, 1.0f);
+                .scroll(Direction.DOWN, 1.0f);
             }
             --maxAttempt;
         }
@@ -277,20 +307,20 @@
                 By.res(RES_PACKAGE_NAME, "text1").text("01. Array")), LONG_TIMEOUT);
         Assert.assertNotNull("Array listview can't be found", array);
         array.click();
+        mListView = mDevice.wait(Until.findObject(By.res(
+                   RES_PACKAGE_NAME, "content")), LONG_TIMEOUT);
+        Assert.assertNotNull("Content pane isn't found to move up", mListView);
     }
 
     // Measures jank for simple listview fling
     @JankTest(beforeTest="selectListsArray", afterTest="goBackHome",
-              expectedFrames=EXPECTED_FRAMES)
+            expectedFrames=EXPECTED_FRAMES)
     @GfxMonitor(processName=PACKAGE_NAME)
     public void testListViewJank() {
         for (int i = 0; i < INNER_LOOP; i++) {
-            UiObject2 listView = mDevice.wait(Until.findObject(By.res(
-                    RES_PACKAGE_NAME, "content")), LONG_TIMEOUT);
-            Assert.assertNotNull("Content pane isn't found to move up", listView);
-            listView.fling(Direction.DOWN);
+            mListView.fling(Direction.DOWN);
             SystemClock.sleep(SHORT_TIMEOUT);
-            listView.fling(Direction.UP);
+            mListView.fling(Direction.UP);
             SystemClock.sleep(SHORT_TIMEOUT);
         }
     }
@@ -307,36 +337,36 @@
     // Measures jank for simple expandable list view expansion
     // Expansion group1, group3 and group4 arbitrarily selected
     @JankTest(beforeTest="selectExpandableListsSimpleAdapter", afterTest="goBackHome",
-              expectedFrames=EXPECTED_FRAMES)
+            expectedFrames=EXPECTED_FRAMES)
     @GfxMonitor(processName=PACKAGE_NAME)
     public void testExapandableListViewJank() {
         for (int i = 0; i < INNER_LOOP; i++) {
-          UiObject2 group1 = mDevice.wait(Until.findObject(By.res(
-                  RES_PACKAGE_NAME, "text1").text("Group 1")), LONG_TIMEOUT);
-          Assert.assertNotNull("Group 1 isn't found to be expanded", group1);
-          group1.click();
-          SystemClock.sleep(SHORT_TIMEOUT);
-          group1.click();
-          SystemClock.sleep(SHORT_TIMEOUT);
-          UiObject2 group3 = mDevice.wait(Until.findObject(By.res(
-                  RES_PACKAGE_NAME, "text1").text("Group 3")), LONG_TIMEOUT);
-          Assert.assertNotNull("Group 3 isn't found to be expanded", group3);
-          group3.click();
-          SystemClock.sleep(SHORT_TIMEOUT);
-          group3.click();
-          SystemClock.sleep(SHORT_TIMEOUT);
-          UiObject2 group4 = mDevice.wait(Until.findObject(By.res(
-                  RES_PACKAGE_NAME, "text1").text("Group 4")), LONG_TIMEOUT);
-          Assert.assertNotNull("Group 4 isn't found to be expanded", group4);
-          group4.click();
-          SystemClock.sleep(SHORT_TIMEOUT);
-          group4.click();
-          SystemClock.sleep(SHORT_TIMEOUT);
-          UiObject2 content = mDevice.wait(Until.findObject(By.res(
-                  RES_PACKAGE_NAME, "content")), LONG_TIMEOUT);
-          Assert.assertNotNull("Content pane isn't found to move up", content);
-          content.fling(Direction.UP);
-          SystemClock.sleep(SHORT_TIMEOUT);
+            UiObject2 group1 = mDevice.wait(Until.findObject(By.res(
+                    RES_PACKAGE_NAME, "text1").text("Group 1")), LONG_TIMEOUT);
+            Assert.assertNotNull("Group 1 isn't found to be expanded", group1);
+            group1.click();
+            SystemClock.sleep(SHORT_TIMEOUT);
+            group1.click();
+            SystemClock.sleep(SHORT_TIMEOUT);
+            UiObject2 group3 = mDevice.wait(Until.findObject(By.res(
+                    RES_PACKAGE_NAME, "text1").text("Group 3")), LONG_TIMEOUT);
+            Assert.assertNotNull("Group 3 isn't found to be expanded", group3);
+            group3.click();
+            SystemClock.sleep(SHORT_TIMEOUT);
+            group3.click();
+            SystemClock.sleep(SHORT_TIMEOUT);
+            UiObject2 group4 = mDevice.wait(Until.findObject(By.res(
+                    RES_PACKAGE_NAME, "text1").text("Group 4")), LONG_TIMEOUT);
+            Assert.assertNotNull("Group 4 isn't found to be expanded", group4);
+            group4.click();
+            SystemClock.sleep(SHORT_TIMEOUT);
+            group4.click();
+            SystemClock.sleep(SHORT_TIMEOUT);
+            UiObject2 content = mDevice.wait(Until.findObject(By.res(
+                    RES_PACKAGE_NAME, "content")), LONG_TIMEOUT);
+            Assert.assertNotNull("Content pane isn't found to move up", content);
+            content.fling(Direction.UP);
+            SystemClock.sleep(SHORT_TIMEOUT);
         }
     }
 }
diff --git a/tests/jank/sysapp/Android.mk b/tests/jank/sysapp/Android.mk
index b84bc10..41c3d13 100644
--- a/tests/jank/sysapp/Android.mk
+++ b/tests/jank/sysapp/Android.mk
@@ -19,7 +19,7 @@
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 LOCAL_MODULE_TAGS := tests
 
-LOCAL_STATIC_JAVA_LIBRARIES := ub-uiautomator ub-janktesthelper timeresult-helper-lib
+LOCAL_STATIC_JAVA_LIBRARIES := ub-uiautomator ub-janktesthelper timeresult-helper-lib app-helpers
 
 LOCAK_SDK_VERSION := current
 
diff --git a/tests/jank/sysapp/src/com/android/sysapp/janktests/CalendarJankTests.java b/tests/jank/sysapp/src/com/android/sysapp/janktests/CalendarJankTests.java
index 253a82a..c69522f 100644
--- a/tests/jank/sysapp/src/com/android/sysapp/janktests/CalendarJankTests.java
+++ b/tests/jank/sysapp/src/com/android/sysapp/janktests/CalendarJankTests.java
@@ -37,6 +37,8 @@
 import android.support.test.uiautomator.UiObjectNotFoundException;
 import android.support.test.uiautomator.Until;
 import android.view.View;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 import junit.framework.Assert;
 import android.support.test.timeresulthelper.TimeResultLogger;
 
@@ -125,7 +127,7 @@
     private void dismissCling() {
         UiObject2 splashScreen = null;
         splashScreen = mDevice.wait(Until.findObject(
-              By.pkg(PACKAGE_NAME).clazz(View.class).desc("Got it")), LONG_TIMEOUT);
+                By.pkg(PACKAGE_NAME).clazz(View.class).desc("Got it")), LONG_TIMEOUT);
         if (splashScreen != null) {
             splashScreen.clickAndWait(Until.newWindow(), SHORT_TIMEOUT);
         }
@@ -137,10 +139,20 @@
             rightArrow.click();
             --counter;
         }
+
+        Pattern pattern = Pattern.compile("GOT IT", Pattern.CASE_INSENSITIVE);
         UiObject2 gotIt = mDevice.wait(Until.findObject(
-              By.res(PACKAGE_NAME, "done_button").text("Got it")), LONG_TIMEOUT);
+                By.res(PACKAGE_NAME, "done_button").text(pattern)), LONG_TIMEOUT);
         if (gotIt != null) {
             gotIt.click();
         }
+
+        pattern = Pattern.compile("DISMISS", Pattern.CASE_INSENSITIVE);
+        UiObject2 dismissSync = mDevice.wait(Until.findObject(
+                By.res(PACKAGE_NAME, "button_dismiss").text(pattern)), LONG_TIMEOUT);
+        if (dismissSync != null) {
+            dismissSync.click();
+        }
+
     }
 }
diff --git a/tests/jank/sysapp/src/com/android/sysapp/janktests/ChromeJankTests.java b/tests/jank/sysapp/src/com/android/sysapp/janktests/ChromeJankTests.java
index 3659dda..9fe6ae1 100644
--- a/tests/jank/sysapp/src/com/android/sysapp/janktests/ChromeJankTests.java
+++ b/tests/jank/sysapp/src/com/android/sysapp/janktests/ChromeJankTests.java
@@ -36,6 +36,7 @@
 import android.support.test.uiautomator.UiObjectNotFoundException;
 import android.support.test.uiautomator.Until;
 import junit.framework.Assert;
+import android.platform.test.helpers.ChromeHelperImpl;
 import android.support.test.timeresulthelper.TimeResultLogger;
 
 /**
@@ -50,6 +51,7 @@
     private static final int EXPECTED_FRAMES = 100;
     private static final String PACKAGE_NAME = "com.android.chrome";
     private UiDevice mDevice;
+    private ChromeHelperImpl chromeHelper;
     private static final File TIMESTAMP_FILE = new File(Environment.getExternalStorageDirectory()
             .getAbsolutePath(), "autotester.log");
     private static final File RESULTS_FILE = new File(Environment.getExternalStorageDirectory()
@@ -82,6 +84,8 @@
 
     public void launchChrome() throws UiObjectNotFoundException, IOException{
         launchApp(PACKAGE_NAME);
+        chromeHelper = new ChromeHelperImpl(getInstrumentation());
+        chromeHelper.dismissInitialDialogs();
         getOverflowMenu();
         TimeResultLogger.writeTimeStampLogStart(String.format("%s-%s",
                 getClass().getSimpleName(), getName()), TIMESTAMP_FILE);
diff --git a/tests/jank/sysapp/src/com/android/sysapp/janktests/GMailJankTests.java b/tests/jank/sysapp/src/com/android/sysapp/janktests/GMailJankTests.java
index 4a5e96c..8f4b8a6 100644
--- a/tests/jank/sysapp/src/com/android/sysapp/janktests/GMailJankTests.java
+++ b/tests/jank/sysapp/src/com/android/sysapp/janktests/GMailJankTests.java
@@ -38,7 +38,9 @@
 import android.support.test.uiautomator.Until;
 import android.widget.ImageButton;
 import junit.framework.Assert;
+import android.platform.test.helpers.GmailHelperImpl;
 import android.support.test.timeresulthelper.TimeResultLogger;
+import java.util.regex.Pattern;
 
 /**
  * Jank test for scrolling gmail inbox mails
@@ -53,6 +55,7 @@
     private static final String PACKAGE_NAME = "com.google.android.gm";
     private static final String RES_PACKAGE_NAME = "android";
     private UiDevice mDevice;
+    private GmailHelperImpl mGmailHelper;
     private static final File TIMESTAMP_FILE = new File(Environment.getExternalStorageDirectory()
             .getAbsolutePath(), "autotester.log");
     private static final File RESULTS_FILE = new File(Environment.getExternalStorageDirectory()
@@ -62,6 +65,7 @@
     public void setUp() throws Exception {
         super.setUp();
         mDevice = UiDevice.getInstance(getInstrumentation());
+        mGmailHelper = new GmailHelperImpl(getInstrumentation());
         mDevice.setOrientationNatural();
     }
 
@@ -81,9 +85,7 @@
 
     public void launchGMail () throws UiObjectNotFoundException {
         launchApp(PACKAGE_NAME);
-        dismissClings();
-        // Need any check for account-name??
-        waitForEmailSync();
+        mGmailHelper.dismissInitialDialogs();
     }
 
     public void prepGMailInboxFling() throws UiObjectNotFoundException, IOException {
@@ -156,13 +158,21 @@
         Assert.assertNotNull("Failed to locate Nav Drawer Openner", navDrawer);
         navDrawer.click();
         // Ensure test is ready to be executed
-        UiObject2 container = getNavigationDrawerContainer();
-        Assert.assertNotNull("Failed to locate Nav drawer container", container);
+        UiObject2 acctListBtn = mDevice.wait(
+                Until.findObject(By.res(PACKAGE_NAME, "account_list_button")),
+                SHORT_TIMEOUT);
+        Assert.assertNotNull("Failed to locate Nav drawer ", acctListBtn);
         TimeResultLogger.writeTimeStampLogStart(String.format("%s-%s",
                 getClass().getSimpleName(), getName()), TIMESTAMP_FILE);
     }
 
     public void afterTestFlingNavDrawer(Bundle metrics) throws IOException {
+        if (!mGmailHelper.closeNavigationDrawer()) {
+            UiObject2 container = getNavigationDrawerContainer();
+            if (container != null) {
+                container.fling(Direction.RIGHT);
+            }
+        }
         TimeResultLogger.writeTimeStampLogEnd(String.format("%s-%s",
                 getClass().getSimpleName(), getName()), TIMESTAMP_FILE);
         TimeResultLogger.writeResultToFile(String.format("%s-%s",
@@ -184,49 +194,14 @@
         }
     }
 
-    private void dismissClings() {
-        UiObject2 welcomeScreenGotIt = mDevice.wait(
-            Until.findObject(By.res(PACKAGE_NAME, "welcome_tour_got_it")), SHORT_TIMEOUT);
-        if (welcomeScreenGotIt != null) {
-            welcomeScreenGotIt.clickAndWait(Until.newWindow(), SHORT_TIMEOUT);
-        }
-        UiObject2 welcomeScreenSkip = mDevice.wait(
-            Until.findObject(By.res(PACKAGE_NAME, "welcome_tour_skip")), SHORT_TIMEOUT);
-        if (welcomeScreenSkip != null) {
-          welcomeScreenSkip.clickAndWait(Until.newWindow(), SHORT_TIMEOUT);
-        }
-        UiObject2 tutorialDone = mDevice.wait(
-                Until.findObject(By.res(PACKAGE_NAME, "action_done")), 2 * SHORT_TIMEOUT);
-        if (tutorialDone != null) {
-            tutorialDone.clickAndWait(Until.newWindow(), SHORT_TIMEOUT);
-        }
-        mDevice.wait(Until.findObject(By.text("CONFIDENTIAL")), 2 * SHORT_TIMEOUT);
-        UiObject2 splash = mDevice.findObject(By.text("Ok, got it"));
-        if (splash != null) {
-            splash.clickAndWait(Until.newWindow(), SHORT_TIMEOUT);
-        }
-    }
-
-    public void waitForEmailSync() {
-        // Wait up to 2 seconds for a "waiting" message to appear
-        mDevice.wait(Until.hasObject(By.text("Waiting for sync")), 2 * SHORT_TIMEOUT);
-        // Wait until any "waiting" messages are gone
-        Assert.assertTrue("'Waiting for sync' timed out",
-                mDevice.wait(Until.gone(By.text("Waiting for sync")), LONG_TIMEOUT * 6));
-        Assert.assertTrue("'Loading' timed out",
-                mDevice.wait(Until.gone(By.text("Loading")), LONG_TIMEOUT * 6));
-    }
 
     public UiObject2 openNavigationDrawer() {
-        UiObject2 navDrawer = null;
-        if (mDevice.getDisplaySizeDp().x < TAB_MIN_WIDTH) {
-            navDrawer = mDevice.wait(Until.findObject(
-                    By.clazz(ImageButton.class).desc("Navigate up")), SHORT_TIMEOUT);
-        } else {
-            navDrawer = mDevice.wait(Until.findObject(
-                    By.clazz(ImageButton.class).desc("Open navigation drawer")), SHORT_TIMEOUT);
+        UiObject2 nav = mDevice.findObject(By.desc(Pattern.compile(
+                "(Open navigation drawer)|(Navigate up)")));
+        if (nav == null) {
+            throw new IllegalStateException("Could not find navigation drawer");
         }
-        return navDrawer;
+        return nav;
     }
 
     public UiObject2 getNavigationDrawerContainer() {
diff --git a/tests/jank/sysapp/src/com/android/sysapp/janktests/YouTubeJankTests.java b/tests/jank/sysapp/src/com/android/sysapp/janktests/YouTubeJankTests.java
index d0614d4..e51f2b9 100644
--- a/tests/jank/sysapp/src/com/android/sysapp/janktests/YouTubeJankTests.java
+++ b/tests/jank/sysapp/src/com/android/sysapp/janktests/YouTubeJankTests.java
@@ -71,7 +71,7 @@
         super.tearDown();
     }
 
-    public void launchApp(String packageName) throws UiObjectNotFoundException{
+    public void launchApp(String packageName) throws UiObjectNotFoundException {
         PackageManager pm = getInstrumentation().getContext().getPackageManager();
         Intent appIntent = pm.getLaunchIntentForPackage(packageName);
         appIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
@@ -115,7 +115,7 @@
     private void dismissCling() {
         // Dismiss the dogfood splash screen that might appear on first start
         UiObject2 newNavigationDoneBtn = mDevice.wait(Until.findObject(
-            By.res(PACKAGE_NAME, "done_button").text("Done")), LONG_TIMEOUT);
+            By.res(PACKAGE_NAME, "done_button")), LONG_TIMEOUT);
         if (newNavigationDoneBtn != null) {
           newNavigationDoneBtn.click();
         }
@@ -136,6 +136,11 @@
                     Until.findObject(By.res(PACKAGE_NAME, "ok").text("OK")), LONG_TIMEOUT);
             Assert.assertNotNull("No 'ok' button to bypass music", ok);
             ok.click();
-      }
+        }
+        UiObject2 laterButton = mDevice.wait(
+            Until.findObject(By.res(PACKAGE_NAME, "later_button")), LONG_TIMEOUT);
+        if (laterButton != null) {
+            laterButton.click();
+         }
     }
 }
diff --git a/tests/jank/sysapp_wear/AndroidManifest.xml b/tests/jank/sysapp_wear/AndroidManifest.xml
index 78d3567..cb90c58 100644
--- a/tests/jank/sysapp_wear/AndroidManifest.xml
+++ b/tests/jank/sysapp_wear/AndroidManifest.xml
@@ -16,11 +16,12 @@
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.wearable.sysapp.janktests">
-
     <application>
         <uses-library android:name="android.test.runner" />
     </application>
-
+    <uses-sdk android:minSdkVersion="23" android:targetSdkVersion="23" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.WAKE_LOCK" />
     <instrumentation
             android:name="android.test.InstrumentationTestRunner"
             android:targetPackage="com.android.wearable.sysapp.janktests"
diff --git a/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/AppLauncherFlingJankTest.java b/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/AppLauncherFlingJankTest.java
new file mode 100644
index 0000000..fceae07
--- /dev/null
+++ b/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/AppLauncherFlingJankTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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.wearable.sysapp.janktests;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+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.Direction;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Jank tests to fling through apps available in the launcher [1st page]
+ */
+public class AppLauncherFlingJankTest extends JankTestBase {
+
+    private UiDevice mDevice;
+    private SysAppTestHelper mHelper;
+    private PowerManager mPm;
+    private WakeLock mWakeLock;
+
+    /*
+     * (non-Javadoc)
+     * @see junit.framework.TestCase#setUp()
+     */
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mDevice = UiDevice.getInstance(getInstrumentation());
+        mHelper = SysAppTestHelper.getInstance(mDevice, this.getInstrumentation());
+        mPm = (PowerManager) getInstrumentation().
+                getContext().getSystemService(Context.POWER_SERVICE);
+        mWakeLock = mPm.newWakeLock(PowerManager.FULL_WAKE_LOCK,
+                AppLauncherFlingJankTest.class.getSimpleName());
+        mWakeLock.acquire();
+    }
+
+    /**
+     * This method ensures the device is taken to Home and launch the apps page (also known as 1st
+     * page) before running the fling test on apps.
+     * @throws RemoteException
+     * @throws TimeoutException
+     */
+    public void openLauncher() throws RemoteException, TimeoutException {
+        mHelper.gotoAppLauncher();
+     }
+
+    /**
+     * Test the jank by flinging in apps screen.
+     * @throws TimeoutException
+     *
+     */
+    @JankTest(beforeTest = "openLauncher", afterTest = "goBackHome",
+        expectedFrames = SysAppTestHelper.EXPECTED_FRAMES)
+    @GfxMonitor(processName = "com.google.android.wearable.app")
+    public void testFlingApps() throws TimeoutException {
+        UiObject2 recyclerViewContents = mDevice.wait(Until.findObject(
+        By.res("com.google.android.wearable.app","launcher_view")), SysAppTestHelper.SHORT_TIMEOUT);
+        for (int i = 0; i < 3; i++) {
+          recyclerViewContents.fling(Direction.DOWN, SysAppTestHelper.FLING_SPEED);
+          recyclerViewContents.fling(Direction.UP, SysAppTestHelper.FLING_SPEED);
+       }
+    }
+
+    // Ensuring that we head back to the first screen before launching the app again
+    public void goBackHome(Bundle metrics) throws RemoteException {
+        mHelper.goBackHome();
+        super.afterTest(metrics);
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see android.test.InstrumentationTestCase#tearDown()
+     */
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+        mWakeLock.release();
+    }
+}
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 10f3bfb..14507d3 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
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2015 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.
@@ -17,15 +17,13 @@
 package com.android.wearable.sysapp.janktests;
 
 import android.os.Bundle;
-import android.os.RemoteException;
-import android.os.SystemClock;
 import android.support.test.jank.GfxMonitor;
 import android.support.test.jank.JankTest;
 import android.support.test.jank.JankTestBase;
 import android.support.test.uiautomator.UiDevice;
 
 /**
- * Janks tests for scrolling & swiping off notification cards on wear
+ * Jank tests for scrolling & swiping off notification cards on wear
  */
 public class CardsJankTest extends JankTestBase {
 
@@ -40,24 +38,22 @@
     protected void setUp() throws Exception {
         super.setUp();
         mDevice = UiDevice.getInstance(getInstrumentation());
-        mHelper = SysAppTestHelper.getInstance(mDevice, this.getInstrumentation().getContext());
+        mHelper = SysAppTestHelper.getInstance(mDevice, this.getInstrumentation());
         mDevice.wakeUp();
-        SystemClock.sleep(SysAppTestHelper.SHORT_TIMEOUT);
     }
 
     // Prepare device to start scrolling by tapping on the screen
     // As this is done using demo cards a tap on screen will stop animation and show
     // home screen
     public void openScrollCard() throws Exception {
-        mHelper.goBackHome();
         mHelper.hasDemoCards();
-        SystemClock.sleep(SysAppTestHelper.SHORT_TIMEOUT);
+        mHelper.swipeUp();
     }
 
     // Measure card scroll jank
 
-    @JankTest(beforeTest = "openScrollCard", afterTest = "goBackHome",
-            expectedFrames = SysAppTestHelper.MIN_FRAMES)
+    @JankTest(beforeLoop = "openScrollCard", afterTest = "goBackHome",
+            expectedFrames = SysAppTestHelper.EXPECTED_FRAMES_CARDS_TEST)
     @GfxMonitor(processName = "com.google.android.wearable.app")
     public void testScrollCard() {
         mHelper.swipeUp();
@@ -69,23 +65,21 @@
         mHelper.hasDemoCards();
         mHelper.swipeUp();
         mHelper.swipeUp();
-        SystemClock.sleep(SysAppTestHelper.SHORT_TIMEOUT);
     }
 
     // Measure jank when dismissing a card
 
-    @JankTest(beforeTest = "openSwipeCard", afterTest = "goBackHome",
-            expectedFrames = SysAppTestHelper.MIN_FRAMES)
+    @JankTest(beforeLoop = "openSwipeCard", afterTest = "goBackHome",
+            expectedFrames = SysAppTestHelper.EXPECTED_FRAMES_CARDS_TEST)
     @GfxMonitor(processName = "com.google.android.wearable.app")
     public void testSwipeCard() {
         mHelper.swipeRight();
     }
 
     // Ensuring that we head back to the first screen before launching the app again
-    public void goBackHome(Bundle metrics) throws RemoteException {
+    public void goBackHome(Bundle metrics) {
         mHelper.goBackHome();
         super.afterTest(metrics);
-        SystemClock.sleep(SysAppTestHelper.LONG_TIMEOUT);
     }
 
     /*
diff --git a/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/QuickSettingsJankTest.java b/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/QuickSettingsJankTest.java
new file mode 100644
index 0000000..ed9c069
--- /dev/null
+++ b/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/QuickSettingsJankTest.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 com.android.wearable.sysapp.janktests;
+
+import android.os.Bundle;
+import android.os.SystemClock;
+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.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+
+import junit.framework.Assert;
+
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Jank tests for Quick settings when pulling down, pulling up the shade. And also when swiping in
+ * quick settings options.
+ */
+public class QuickSettingsJankTest extends JankTestBase {
+
+    private UiDevice mDevice;
+    private SysAppTestHelper mHelper;
+
+    private static final String WEARABLE_APP_PACKAGE = "com.google.android.wearable.app";
+    private static final String QUICK_SETTINGS_LAUNCHED_INDICATOR = "settings_icon";
+
+    /*
+     * (non-Javadoc)
+     * @see junit.framework.TestCase#setUp()
+     */
+    @Override
+    protected void setUp() throws Exception {
+        mDevice = UiDevice.getInstance(getInstrumentation());
+        mHelper = SysAppTestHelper.getInstance(mDevice, this.getInstrumentation());
+        mDevice.wakeUp();
+        super.setUp();
+    }
+
+    private void isQuickSettingShadeLaunched() throws TimeoutException {
+        SystemClock.sleep(SysAppTestHelper.SHORT_TIMEOUT + SysAppTestHelper.SHORT_TIMEOUT); //Wait until date & battery info transitions to page indicator
+        UiObject2 quickSettingsShade = mDevice.wait(
+                Until.findObject(By.res(WEARABLE_APP_PACKAGE, QUICK_SETTINGS_LAUNCHED_INDICATOR)),
+                SysAppTestHelper.SHORT_TIMEOUT);
+        Assert.assertNotNull("Quick settings shade not launched", quickSettingsShade);
+
+    }
+
+    // Prepare device to be on Home before pulling down Quick settings shade
+    public void startFromHome() {
+        mHelper.goBackHome();
+    }
+
+    // Verify jank while pulling down quick settings
+    @JankTest(beforeLoop = "startFromHome", afterTest = "goBackHome",
+            expectedFrames = SysAppTestHelper.EXPECTED_FRAMES_CARDS_TEST)
+    @GfxMonitor(processName = WEARABLE_APP_PACKAGE)
+    public void testPullDownQuickSettings() {
+        mHelper.swipeDown();
+    }
+
+    // Prepare device by pulling down the quick settings shade.
+    public void openPullUpQuickSettings() throws TimeoutException {
+        mHelper.goBackHome();
+        mHelper.swipeDown();
+        isQuickSettingShadeLaunched();
+    }
+
+    // Verify jank while pulling up quick settings
+    @JankTest(beforeLoop = "openPullUpQuickSettings", afterTest = "goBackHome",
+            expectedFrames = SysAppTestHelper.EXPECTED_FRAMES_CARDS_TEST)
+    @GfxMonitor(processName = WEARABLE_APP_PACKAGE)
+    public void testPullUpQuickSettings() {
+        mHelper.swipeUp();
+    }
+
+    // Ensuring that we head back to the first screen before launching the app again
+    public void goBackHome(Bundle metrics) {
+        mHelper.goBackHome();
+        super.afterTest(metrics);
+    }
+}
diff --git a/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/SettingsFlingJankTest.java b/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/SettingsFlingJankTest.java
new file mode 100644
index 0000000..5311426
--- /dev/null
+++ b/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/SettingsFlingJankTest.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 com.android.wearable.sysapp.janktests;
+
+import android.os.Bundle;
+import android.os.SystemClock;
+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.Direction;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Jank tests to fling through Settings app on clockwork device
+ */
+public class SettingsFlingJankTest extends JankTestBase {
+
+    private UiDevice mDevice;
+    private SysAppTestHelper mHelper;
+
+    // Settings app resources
+    private static final String CLOCK_SETTINGS_PACKAGE =
+        "com.google.android.apps.wearable.settings";
+    private static final String CLOCK_SETTINGS_ACTIVITY =
+        "com.google.android.clockwork.settings.SettingsActivity";
+
+    /*
+     * (non-Javadoc)
+     * @see junit.framework.TestCase#setUp()
+     */
+    @Override
+    protected void setUp() throws Exception {
+        mDevice = UiDevice.getInstance(getInstrumentation());
+        mHelper = SysAppTestHelper.getInstance(mDevice, this.getInstrumentation());
+        mDevice.wakeUp();
+        super.setUp();
+    }
+
+    // Prepare device to launch Settings app and scroll through bottom to start fling test
+    public void openSettingsApp() {
+        mHelper.launchActivity(CLOCK_SETTINGS_PACKAGE, CLOCK_SETTINGS_ACTIVITY);
+        SystemClock.sleep(SysAppTestHelper.SHORT_TIMEOUT);
+    }
+
+    /**
+     * Test the jank by flinging in settings screen.
+     * @throws TimeoutException
+     *
+     */
+    @JankTest(beforeTest = "openSettingsApp", afterTest = "goBackHome",
+        expectedFrames = SysAppTestHelper.EXPECTED_FRAMES)
+    @GfxMonitor(processName = CLOCK_SETTINGS_PACKAGE)
+    public void testSettingsApp() throws TimeoutException {
+          UiObject2 recyclerViewContents = mDevice.wait(Until.findObject(
+              By.res(CLOCK_SETTINGS_PACKAGE,"wheel")), SysAppTestHelper.SHORT_TIMEOUT);
+          for (int i = 0; i < 3; i++) {
+              recyclerViewContents.fling(Direction.DOWN, SysAppTestHelper.FLING_SPEED);
+              recyclerViewContents.fling(Direction.UP, SysAppTestHelper.FLING_SPEED);
+         }
+    }
+
+    // Ensuring that we head back to the first screen before launching the app again
+    public void goBackHome(Bundle metrics) {
+        mHelper.goBackHome();
+        super.afterTest(metrics);
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see android.test.InstrumentationTestCase#tearDown()
+     */
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+    }
+}
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 5421602..9901a0b 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
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2015 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,27 +16,41 @@
 
 package com.android.wearable.sysapp.janktests;
 
+import android.app.Instrumentation;
 import android.content.ComponentName;
-import android.content.Context;
 import android.content.Intent;
-import android.os.RemoteException;
 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;
 
+import java.util.concurrent.TimeoutException;
+
 /**
- * Helper for all they system apps tests
+ * Helper for all the system apps jank tests
  */
 public class SysAppTestHelper {
 
-    public static final int MIN_FRAMES = 20;
+    private static final String LOG_TAG = SysAppTestHelper.class.getSimpleName();
+    public static final int EXPECTED_FRAMES_CARDS_TEST = 20;
+    public static final int EXPECTED_FRAMES = 100;
     public static final int LONG_TIMEOUT = 5000;
     public static final int SHORT_TIMEOUT = 500;
-    private static final long NEW_CARD_TIMEOUT_MS = 10 * 1000; // 10s
-    private static final int CARD_SWIPE_STEPS = 20;
+    public static final int FLING_SPEED = 5000;
+    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 LAUNCHER_VIEW_NAME = "launcher_view";
+    private static final String CARD_VIEW_NAME = "activity_view";
+    private static final String QUICKSETTING_VIEW_NAME = "settings_icon";
 
     // Demo card selectors
     private static final UiSelector CARD_SELECTOR = new UiSelector()
@@ -45,29 +59,44 @@
             .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 UiDevice mDevice = null;
-    private Context mContext = null; // Currently not used but for further tests may be useful.
+    private Instrumentation instrumentation = null;
     private UiObject mCard = null;
     private UiObject mTitle = null;
     private UiObject mClock = null;
+    private UiObject mIcon = null;
+    private UiObject mText = null;
+    private UiObject mStatus = null;
     private Intent mIntent = null;
     private static SysAppTestHelper sysAppTestHelperInstance;
 
     /**
      * @param mDevice
-     * @param mContext
+     * @param instrumentation
      */
-    private SysAppTestHelper(UiDevice mDevice, Context mContext) {
+    private SysAppTestHelper(UiDevice mDevice, Instrumentation instrumentation) {
         super();
         this.mDevice = mDevice;
-        this.mContext = mContext;
+        this.instrumentation = instrumentation;
         mIntent = new Intent();
+        mCard = mDevice.findObject(CARD_SELECTOR);
+        mTitle = mDevice.findObject(TITLE_SELECTOR);
+        mClock = mDevice.findObject(CLOCK_SELECTOR);
+        mIcon = mDevice.findObject(ICON_SELECTOR);
+        mText = mDevice.findObject(TEXT_SELECTOR);
+        mStatus = mDevice.findObject(STATUS_BAR_SELECTOR);
     }
 
-    public static SysAppTestHelper getInstance(UiDevice device, Context context) {
+    public static SysAppTestHelper getInstance(UiDevice device, Instrumentation instrumentation) {
         if (sysAppTestHelperInstance == null) {
-            sysAppTestHelperInstance = new SysAppTestHelper(device, context);
+            sysAppTestHelperInstance = new SysAppTestHelper(device, instrumentation);
         }
         return sysAppTestHelperInstance;
     }
@@ -100,6 +129,7 @@
     public void flingUp() {
         mDevice.swipe(mDevice.getDisplayWidth() / 2, mDevice.getDisplayHeight() / 2 + 50,
                 mDevice.getDisplayWidth() / 2, 0, 5); // fast speed
+        SystemClock.sleep(SHORT_TIMEOUT);
     }
 
     public void flingDown() {
@@ -109,16 +139,33 @@
     }
 
     // Helper method to go back to home screen
-    public void goBackHome() throws RemoteException {
+    public void goBackHome() {
+        String launcherPackage = mDevice.getLauncherPackageName();
+        UiObject2 homeScreen = mDevice.findObject(By.res(launcherPackage, HOME_INDICATOR));
         int count = 0;
-        mClock = null;
-        while (mClock == null && count++ < 5) {
-            mDevice.sleep();
-            SystemClock.sleep(LONG_TIMEOUT);
-            mDevice.wakeUp();
-            SystemClock.sleep(SHORT_TIMEOUT + SHORT_TIMEOUT);
-            mClock = mDevice.findObject(CLOCK_SELECTOR); // Ensure device is really on Home screen
+        while (homeScreen == null && count < 5) {
+            mDevice.pressBack();
+            homeScreen = mDevice.findObject(By.res(launcherPackage, HOME_INDICATOR));
+            count ++;
         }
+
+        // TODO (yuanlang@) Delete the following hacky codes after charging icon issue fixed
+        // Make sure we're not in the launcher
+        homeScreen = mDevice.findObject(By.res(launcherPackage, LAUNCHER_VIEW_NAME));
+        if (homeScreen != null) {
+            mDevice.pressBack();
+        }
+        // Make sure we're not in cards view
+        homeScreen = mDevice.findObject(By.res(launcherPackage, CARD_VIEW_NAME));
+        if (homeScreen != null) {
+            mDevice.pressBack();
+        }
+        // Make sure we're not in the quick settings
+        homeScreen = mDevice.findObject(By.res(launcherPackage, QUICKSETTING_VIEW_NAME));
+        if (homeScreen != null) {
+            mDevice.pressBack();
+        }
+        SystemClock.sleep(LONG_TIMEOUT);
     }
 
     // Helper method to verify if there are any Demo cards.
@@ -127,29 +174,58 @@
     // more than one card.
     public void hasDemoCards() throws Exception {
         // Device should be pre-loaded with demo cards.
-        // Start the intent to go to home screen
-        mCard = mDevice.findObject(CARD_SELECTOR);
-        mTitle = mDevice.findObject(TITLE_SELECTOR);
-        mClock = mDevice.findObject(CLOCK_SELECTOR);
 
-        if (mClock.waitForExists(NEW_CARD_TIMEOUT_MS)) {
-            mClock.swipeUp(CARD_SWIPE_STEPS);
+        goBackHome(); // Start by going to Home.
+
+        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.
         }
 
         // 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.
+        // 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 there are no Demo cards, reload them.
+            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",
-                (mCard.waitForExists(NEW_CARD_TIMEOUT_MS)
-                        || mTitle.waitForExists(NEW_CARD_TIMEOUT_MS)));
+                (mTitle.waitForExists(NEW_CARD_TIMEOUT_MS)
+                        || mIcon.waitForExists(NEW_CARD_TIMEOUT_MS)
+                        || mText.waitForExists(NEW_CARD_TIMEOUT_MS)));
+    }
+
+    // This will ensure to reload notification cards by launching NotificationsGeneratorWear app
+    // when there are insufficient cards.
+    private void reloadDemoCards() {
+        mIntent.setAction(RELOAD_NOTIFICATION_CARD_INTENT);
+        instrumentation.getContext().sendBroadcast(mIntent);
+        SystemClock.sleep(LONG_TIMEOUT);
     }
 
     public void launchActivity(String appPackage, String activityToLaunch) {
         mIntent.setAction("android.intent.action.MAIN");
         mIntent.setComponent(new ComponentName(appPackage, activityToLaunch));
         mIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        mContext.startActivity(mIntent);
+        instrumentation.getContext().startActivity(mIntent);
     }
 
+    // Helper method to goto app launcher and verifies you are there.
+    public void gotoAppLauncher() throws TimeoutException {
+        goBackHome();
+        mDevice.pressKeyCode(KeyEvent.KEYCODE_BACK);
+        UiObject2 appLauncher = mDevice.wait(Until.findObject(By.text("Agenda")),
+                SysAppTestHelper.LONG_TIMEOUT);
+        Assert.assertNotNull("App launcher not launched", appLauncher);
+    }
 }
diff --git a/tests/jank/uibench/Android.mk b/tests/jank/uibench/Android.mk
new file mode 100644
index 0000000..c46630b
--- /dev/null
+++ b/tests/jank/uibench/Android.mk
@@ -0,0 +1,26 @@
+# Copyright 2015 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
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT 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_PACKAGE_NAME := UiBenchJankTests
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_STATIC_JAVA_LIBRARIES := ub-uiautomator ub-janktesthelper
+
+LOCAL_SDK_VERSION := current
+
+include $(BUILD_PACKAGE)
diff --git a/tests/jank/uibench/AndroidManifest.xml b/tests/jank/uibench/AndroidManifest.xml
new file mode 100644
index 0000000..4cf4e01
--- /dev/null
+++ b/tests/jank/uibench/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.uibench.janktests">
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <uses-sdk android:minSdkVersion="19"
+          android:targetSdkVersion="23"/>
+
+    <instrumentation
+            android:name="android.test.InstrumentationTestRunner"
+            android:targetPackage="com.android.uibench.janktests"
+            android:label="Platform UiBench Jank Tests" />
+</manifest>
diff --git a/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchJankTests.java b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchJankTests.java
new file mode 100644
index 0000000..7990f7d
--- /dev/null
+++ b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchJankTests.java
@@ -0,0 +1,173 @@
+/*
+ * 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 com.android.uibench.janktests;
+
+import static com.android.uibench.janktests.UiBenchJankTestsHelper.EXPECTED_FRAMES;
+import static com.android.uibench.janktests.UiBenchJankTestsHelper.PACKAGE_NAME;
+
+import android.os.SystemClock;
+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.UiDevice;
+import android.support.test.uiautomator.Until;
+import android.widget.ListView;
+import junit.framework.Assert;
+
+/**
+ * Jank benchmark General tests for UiBench app
+ */
+
+public class UiBenchJankTests extends JankTestBase {
+
+    private UiDevice mDevice;
+    private UiBenchJankTestsHelper mHelper;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mDevice = UiDevice.getInstance(getInstrumentation());
+        mDevice.setOrientationNatural();
+        mHelper = UiBenchJankTestsHelper.getInstance(
+                this.getInstrumentation().getContext(), mDevice);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        mDevice.unfreezeRotation();
+        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);
+        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);
+    }
+
+    // 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);
+    }
+
+    // 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);
+    }
+
+    // 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);
+    }
+
+    // Open Trivial Animation from General
+    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);
+    }
+
+    // Open Trivial listview from General
+    public void openTrivialListView() {
+        mHelper.launchActivity("TrivialListActivity",
+                "General/Trivial ListView");
+        mHelper.mContents = mDevice.wait(Until.findObject(
+                By.res("android", "content")), mHelper.TIMEOUT);
+        Assert.assertNotNull("Trivial ListView isn't found in General", mHelper.mContents);
+    }
+
+    // Test trivialListView fling
+    @JankTest(beforeTest = "openTrivialListView", expectedFrames = EXPECTED_FRAMES)
+    @GfxMonitor(processName = PACKAGE_NAME)
+    public void testTrivialListViewFling() {
+        mHelper.flingUpDown(mHelper.mContents, mHelper.SHORT_TIMEOUT, 2);
+    }
+
+    // Open Trivial Recycler List View from General
+    public void openTrivialRecyclerListView() {
+        mHelper.launchActivity("TrivialRecyclerViewActivity",
+                "General/Trivial Recycler ListView");
+        mHelper.mContents = mDevice.wait(Until.findObject(
+                By.res("android", "content")), mHelper.TIMEOUT);
+        Assert.assertNotNull("Trivial Recycler ListView isn't found in General",
+                mHelper.mContents);
+    }
+
+    // Test trivialRecyclerListView fling
+    @JankTest(beforeTest = "openTrivialRecyclerListView", expectedFrames = EXPECTED_FRAMES)
+    @GfxMonitor(processName = PACKAGE_NAME)
+    public void testTrivialRecyclerListViewFling() {
+        mHelper.flingUpDown(mHelper.mContents, mHelper.SHORT_TIMEOUT, 2);
+    }
+
+    // Open Inflation Listview contents
+    public void openInflatingListView() {
+        mHelper.launchActivity("InflatingListActivity",
+                "Inflation/Inflating ListView");
+        mHelper.mContents = mDevice.wait(Until.findObject(
+                By.res("android", "content")), mHelper.TIMEOUT);
+        Assert.assertNotNull("Inflating ListView isn't found in Inflation",
+                mHelper.mContents);
+    }
+
+    // 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);
+    }
+
+}
diff --git a/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchJankTestsHelper.java b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchJankTestsHelper.java
new file mode 100644
index 0000000..aca7d1f
--- /dev/null
+++ b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchJankTestsHelper.java
@@ -0,0 +1,94 @@
+/*
+ * 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 com.android.uibench.janktests;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.SystemClock;
+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 junit.framework.Assert;
+/**
+ * Jank benchmark tests helper for UiBench app
+ */
+
+public class UiBenchJankTestsHelper {
+    public static final int LONG_TIMEOUT = 5000;
+    public static final int TIMEOUT = 250;
+    public static final int SHORT_TIMEOUT = 2000;
+    public static final int EXPECTED_FRAMES = 100;
+
+    public static final String PACKAGE_NAME = "com.android.test.uibench";
+
+    private static UiBenchJankTestsHelper mInstance;
+    private static UiDevice mDevice;
+    private Context mContext;
+    protected UiObject2 mContents;
+
+    private UiBenchJankTestsHelper(Context context, UiDevice device) {
+        mContext = context;
+        mDevice = device;
+    }
+
+    public static UiBenchJankTestsHelper getInstance(Context context, UiDevice device) {
+        if (mInstance == null) {
+            mInstance = new UiBenchJankTestsHelper(context, device);
+        }
+        return mInstance;
+    }
+
+    /**
+     * Launch activity using intent
+     * @param activityName
+     * @param verifyText
+     */
+    public void launchActivity(String activityName, String verifyText) {
+        ComponentName cn = new ComponentName(PACKAGE_NAME,
+                String.format("%s.%s", PACKAGE_NAME, activityName));
+        Intent intent = new Intent(Intent.ACTION_MAIN);
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
+        intent.setComponent(cn);
+        // Launch the activity
+        mContext.startActivity(intent);
+        UiObject2 expectedTextCmp = mDevice.wait(Until.findObject(
+                By.text(verifyText)), LONG_TIMEOUT);
+        Assert.assertNotNull(String.format("Issue in opening %s", activityName),
+                expectedTextCmp);
+    }
+
+    /**
+     * 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) {
+        for (int count = 0; count < flingCount; count++) {
+            SystemClock.sleep(timeout);
+            content.fling(Direction.DOWN);
+            SystemClock.sleep(timeout);
+            content.fling(Direction.UP);
+        }
+    }
+
+}
diff --git a/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchRenderingJankTests.java b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchRenderingJankTests.java
new file mode 100644
index 0000000..c825116
--- /dev/null
+++ b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchRenderingJankTests.java
@@ -0,0 +1,85 @@
+/*
+ * 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 com.android.uibench.janktests;
+
+import static com.android.uibench.janktests.UiBenchJankTestsHelper.EXPECTED_FRAMES;
+import static com.android.uibench.janktests.UiBenchJankTestsHelper.PACKAGE_NAME;
+
+import android.os.SystemClock;
+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.UiDevice;
+import android.support.test.uiautomator.Until;
+import android.widget.ListView;
+import junit.framework.Assert;
+
+/**
+ * Jank benchmark Rendering tests for UiBench app
+ */
+
+public class UiBenchRenderingJankTests extends JankTestBase {
+
+    private UiDevice mDevice;
+    private UiBenchJankTestsHelper mHelper;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mDevice = UiDevice.getInstance(getInstrumentation());
+        mDevice.setOrientationNatural();
+        mHelper = UiBenchJankTestsHelper.getInstance(this.getInstrumentation().getContext(),
+                mDevice);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        mDevice.unfreezeRotation();
+        super.tearDown();
+    }
+
+    // Open Bitmap Upload
+    public void openBitmapUpload() {
+        mHelper.launchActivity("BitmapUploadActivity",
+                "Rendering/Bitmap Upload");
+    }
+
+    // Test Bitmap Upload jank
+    @JankTest(beforeTest = "openBitmapUpload", expectedFrames = EXPECTED_FRAMES)
+    @GfxMonitor(processName = PACKAGE_NAME)
+    public void testBitmapUploadJank() {
+        SystemClock.sleep(mHelper.LONG_TIMEOUT * 5);
+    }
+
+    // Open Shadow Grid
+    public void openRenderingList() {
+        mHelper.launchActivity("ShadowGridActivity",
+                "Rendering/Shadow Grid");
+        mHelper.mContents = mDevice.wait(Until.findObject(
+                By.clazz(ListView.class)), mHelper.TIMEOUT);
+        Assert.assertNotNull("Shadow Grid list isn't found", mHelper.mContents);
+    }
+
+    // Test Shadow Grid fling
+    @JankTest(beforeTest = "openRenderingList", expectedFrames = EXPECTED_FRAMES)
+    @GfxMonitor(processName = PACKAGE_NAME)
+    public void testShadowGridListFling() {
+        mHelper.flingUpDown(mHelper.mContents, mHelper.SHORT_TIMEOUT, 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
new file mode 100644
index 0000000..b730aec
--- /dev/null
+++ b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchTextJankTests.java
@@ -0,0 +1,103 @@
+/*
+ * 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 com.android.uibench.janktests;
+
+import static com.android.uibench.janktests.UiBenchJankTestsHelper.EXPECTED_FRAMES;
+import static com.android.uibench.janktests.UiBenchJankTestsHelper.PACKAGE_NAME;
+
+import android.os.SystemClock;
+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.UiDevice;
+import android.support.test.uiautomator.Until;
+import android.widget.ListView;
+import junit.framework.Assert;
+
+/**
+ * Jank benchmark Text tests for UiBench app
+ */
+
+public class UiBenchTextJankTests extends JankTestBase {
+
+    private UiDevice mDevice;
+    private UiBenchJankTestsHelper mHelper;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mDevice = UiDevice.getInstance(getInstrumentation());
+        mDevice.setOrientationNatural();
+        mHelper = UiBenchJankTestsHelper.getInstance(
+                this.getInstrumentation().getContext(), mDevice);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        mDevice.unfreezeRotation();
+        super.tearDown();
+    }
+
+    // Open EditText Typing
+    public void openEditTextTyping() {
+        mHelper.launchActivity("EditTextTypeActivity",
+                "Text/EditText Typing");
+    }
+
+    // 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);
+    }
+
+    // Open Layout Cache High Hitrate
+    public void openLayoutCacheHighHitrate() {
+        mHelper.launchActivity("TextCacheHighHitrateActivity",
+                "Text/Layout Cache High Hitrate");
+        mHelper.mContents = mDevice.wait(Until.findObject(
+                By.clazz(ListView.class)), mHelper.TIMEOUT);
+        Assert.assertNotNull("LayoutCacheHighHitrateContents isn't found",
+                mHelper.mContents);
+    }
+
+    // Test Layout Cache High Hitrate fling
+    @JankTest(beforeTest = "openLayoutCacheHighHitrate", expectedFrames = EXPECTED_FRAMES)
+    @GfxMonitor(processName = PACKAGE_NAME)
+    public void testLayoutCacheHighHitrateFling() {
+        mHelper.flingUpDown(mHelper.mContents, mHelper.SHORT_TIMEOUT, 3);
+    }
+
+    // Open Layout Cache Low Hitrate
+    public void openLayoutCacheLowHitrate() {
+        mHelper.launchActivity("TextCacheLowHitrateActivity",
+                "Text/Layout Cache Low Hitrate");
+        mHelper.mContents = mDevice.wait(Until.findObject(
+                By.clazz(ListView.class)), mHelper.TIMEOUT);
+        Assert.assertNotNull("LayoutCacheLowHitrateContents isn't found",
+                mHelper.mContents);
+    }
+
+    // Test Layout Cache Low Hitrate fling
+    @JankTest(beforeTest = "openLayoutCacheLowHitrate", expectedFrames = EXPECTED_FRAMES)
+    @GfxMonitor(processName = PACKAGE_NAME)
+    public void testLayoutCacheLowHitrateFling() {
+        mHelper.flingUpDown(mHelper.mContents, mHelper.SHORT_TIMEOUT, 3);
+    }
+
+}
diff --git a/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchTransitionsJankTests.java b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchTransitionsJankTests.java
new file mode 100644
index 0000000..fccb8c6
--- /dev/null
+++ b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchTransitionsJankTests.java
@@ -0,0 +1,83 @@
+/*
+ * 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 com.android.uibench.janktests;
+
+import static com.android.uibench.janktests.UiBenchJankTestsHelper.EXPECTED_FRAMES;
+import static com.android.uibench.janktests.UiBenchJankTestsHelper.PACKAGE_NAME;
+
+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.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import junit.framework.Assert;
+
+/**
+ * Jank benchmark Text tests for UiBench app
+ */
+
+public class UiBenchTransitionsJankTests extends JankTestBase {
+
+    private UiDevice mDevice;
+    private UiBenchJankTestsHelper mHelper;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mDevice = UiDevice.getInstance(getInstrumentation());
+        mDevice.setOrientationNatural();
+        mHelper = UiBenchJankTestsHelper.getInstance(
+                this.getInstrumentation().getContext(), mDevice);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        mDevice.unfreezeRotation();
+        super.tearDown();
+    }
+
+    // Open Transitions
+    public void openActivityTransition() {
+        mHelper.launchActivity("ActivityTransition",
+                "Transitions/Activity Transition");
+    }
+
+    // Get the image to click
+    public void clickImage(String imageName) {
+        UiObject2 image = mDevice.wait(Until.findObject(
+                By.res(mHelper.PACKAGE_NAME, imageName)), mHelper.TIMEOUT);
+        Assert.assertNotNull(imageName + "Image not found", image);
+        image.clickAndWait(Until.newWindow(), mHelper.TIMEOUT);
+        mDevice.pressBack();
+    }
+
+    // Measures jank for activity transition animation
+    @JankTest(beforeTest = "openActivityTransition", expectedFrames = EXPECTED_FRAMES)
+    @GfxMonitor(processName = PACKAGE_NAME)
+    public void testActivityTransitionsAnimation() {
+        clickImage("ducky");
+        clickImage("woot");
+        clickImage("ball");
+        clickImage("block");
+        clickImage("jellies");
+        clickImage("mug");
+        clickImage("pencil");
+        clickImage("scissors");
+    }
+}
diff --git a/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchWebView.java b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchWebView.java
new file mode 100644
index 0000000..659577c
--- /dev/null
+++ b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchWebView.java
@@ -0,0 +1,68 @@
+/*
+ * 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.uibench.janktests;
+
+import static com.android.uibench.janktests.UiBenchJankTestsHelper.EXPECTED_FRAMES;
+import static com.android.uibench.janktests.UiBenchJankTestsHelper.PACKAGE_NAME;
+
+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.UiDevice;
+import android.support.test.uiautomator.Until;
+
+/**
+ * Jank benchmark WebView tests from UiBench app
+ */
+
+public class UiBenchWebView extends JankTestBase {
+
+    private UiDevice mDevice;
+    private UiBenchJankTestsHelper mHelper;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mDevice = UiDevice.getInstance(getInstrumentation());
+        mDevice.setOrientationNatural();
+        mHelper = UiBenchJankTestsHelper.getInstance(this.getInstrumentation().getContext(),
+                mDevice);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        mDevice.unfreezeRotation();
+        super.tearDown();
+    }
+
+    // Open Scrollable WebView from WebView test
+    public void openScrollableWebView() {
+        mHelper.launchActivity("ScrollableWebViewActivity",
+                "WebView/Scrollable WebView");
+        mHelper.mContents = mDevice.wait(Until.findObject(
+                By.res("android", "content")), mHelper.TIMEOUT);
+    }
+
+    // Test Scrollable WebView fling
+    @JankTest(beforeTest = "openScrollableWebView", expectedFrames = EXPECTED_FRAMES)
+    @GfxMonitor(processName = PACKAGE_NAME)
+    public void testWebViewFling() {
+        mHelper.flingUpDown(mHelper.mContents, mHelper.SHORT_TIMEOUT, 1);
+    }
+
+}
diff --git a/tests/jank/uibench_wear/AndroidManifest.xml b/tests/jank/uibench_wear/AndroidManifest.xml
index 9e71e81..231d519 100644
--- a/tests/jank/uibench_wear/AndroidManifest.xml
+++ b/tests/jank/uibench_wear/AndroidManifest.xml
@@ -20,6 +20,8 @@
     <application>
         <uses-library android:name="android.test.runner" />
     </application>
+    <uses-sdk android:minSdkVersion="23" android:targetSdkVersion="23" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
 
     <instrumentation
             android:name="android.test.InstrumentationTestRunner"
diff --git a/tests/jank/uibench_wear/src/com/android/wearable/uibench/janktests/UiBenchJankTests.java b/tests/jank/uibench_wear/src/com/android/wearable/uibench/janktests/UiBenchJankTests.java
index 8355c31..d61ddf9 100644
--- a/tests/jank/uibench_wear/src/com/android/wearable/uibench/janktests/UiBenchJankTests.java
+++ b/tests/jank/uibench_wear/src/com/android/wearable/uibench/janktests/UiBenchJankTests.java
@@ -193,18 +193,8 @@
     // Open Inflation Listview contents
     public void openInflatingListView() {
         mHelper.launchUiBench();
-        UiObject2 inflation = mDevice.wait(Until.findObject(
-               By.res(mHelper.RES_PACKAGE_NAME, "text1").text("Inflation")), mHelper.TIMEOUT);
-        Assert.assertNotNull("Inflation isn't found in UiBench", inflation);
-        inflation.click();
-        SystemClock.sleep(mHelper.TIMEOUT);
-        UiObject2 inflatingListView = mDevice.wait(Until.findObject(
-                By.res(mHelper.RES_PACKAGE_NAME, "text1").text("Inflating ListView")),
-                    mHelper.TIMEOUT);
-        Assert.assertNotNull("Inflating ListView Contents isn't found in Inflation",
-                inflatingListView);
-        inflatingListView.click();
-        SystemClock.sleep(mHelper.TIMEOUT);
+        mHelper.openTextInList("Inflation");
+        mHelper.openTextInList("Inflating ListView");
     }
 
     // Test Inflating List View fling
diff --git a/tests/jank/uibench_wear/src/com/android/wearable/uibench/janktests/UiBenchJankTestsHelper.java b/tests/jank/uibench_wear/src/com/android/wearable/uibench/janktests/UiBenchJankTestsHelper.java
index 1d2f43e..a1f4902 100644
--- a/tests/jank/uibench_wear/src/com/android/wearable/uibench/janktests/UiBenchJankTestsHelper.java
+++ b/tests/jank/uibench_wear/src/com/android/wearable/uibench/janktests/UiBenchJankTestsHelper.java
@@ -42,7 +42,8 @@
 
     public static final String RES_PACKAGE_NAME = "android";
     public static final String PACKAGE_NAME = "com.android.test.uibench";
-    public static final String CLOCK_BAR_NAME = "clock_bar";
+    public static final String ROOT_NAME = "root";
+    public static final String LAUNCHER_VIEW_NAME = "launcher_view";
     public static final String TEXT_OBJECT_NAME = "text1";
     public static final String UIBENCH_OBJECT_NAME = "UiBench";
 
@@ -77,22 +78,26 @@
         UiObject2 initScreen = mDevice.wait(Until.findObject(By.text(UIBENCH_OBJECT_NAME)), 2000);
         int counter = 3;
         while (initScreen == null && --counter > 0) {
-            swipeRight();
+            mDevice.pressBack();
             initScreen = mDevice.wait(Until.findObject(By.text(UIBENCH_OBJECT_NAME)), 2000);
         }
     }
 
     // Helper method to go back to home screen
     public void goBackHome() throws UiObjectNotFoundException {
-
         String launcherPackage = mDevice.getLauncherPackageName();
-        UiObject2 homeScreen = null;
+        UiObject2 homeScreen = mDevice.findObject(By.res(launcherPackage, ROOT_NAME));
         int count = 0;
         while (homeScreen == null && count < 5) {
-            swipeRight();
-            homeScreen = mDevice.findObject(By.res(launcherPackage,CLOCK_BAR_NAME));
+            mDevice.pressBack();
+            homeScreen = mDevice.findObject(By.res(launcherPackage, ROOT_NAME));
             count ++;
         }
+        // Make sure we're not in the launcher
+        homeScreen = mDevice.findObject(By.res(launcherPackage, LAUNCHER_VIEW_NAME));
+        if (homeScreen != null) {
+            mDevice.pressBack();
+        }
     }
 
     public void openTextInList(String itemName) {
@@ -114,7 +119,6 @@
         Assert.assertNotNull(itemName + ": isn't found", component);
         component.clickAndWait(Until.newWindow(), LONG_TIMEOUT);
         SystemClock.sleep(TIMEOUT);
-
     }
 
     public void swipeRight() {
diff --git a/tests/jank/uibench_wear/src/com/android/wearable/uibench/janktests/UiBenchTextJankTests.java b/tests/jank/uibench_wear/src/com/android/wearable/uibench/janktests/UiBenchTextJankTests.java
index 1a2ee84..afabd0d 100644
--- a/tests/jank/uibench_wear/src/com/android/wearable/uibench/janktests/UiBenchTextJankTests.java
+++ b/tests/jank/uibench_wear/src/com/android/wearable/uibench/janktests/UiBenchTextJankTests.java
@@ -16,7 +16,10 @@
 
 package com.android.wearable.uibench.janktests;
 
-import android.content.Intent;
+import static com.android.wearable.uibench.janktests.UiBenchJankTestsHelper.EXPECTED_FRAMES;
+import static com.android.wearable.uibench.janktests.UiBenchJankTestsHelper.PACKAGE_NAME;
+
+import android.os.Build.VERSION;
 import android.os.Bundle;
 import android.os.RemoteException;
 import android.os.SystemClock;
@@ -24,17 +27,17 @@
 import android.support.test.jank.JankTest;
 import android.support.test.jank.JankTestBase;
 import android.support.test.uiautomator.By;
-import android.support.test.uiautomator.StaleObjectException;
 import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.StaleObjectException;
 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.view.KeyEvent;
 import android.widget.ListView;
 
 import com.android.wearable.uibench.janktests.UiBenchJankTestsHelper;
-import static com.android.wearable.uibench.janktests.UiBenchJankTestsHelper.PACKAGE_NAME;
-import static com.android.wearable.uibench.janktests.UiBenchJankTestsHelper.EXPECTED_FRAMES;
+
 import junit.framework.Assert;
 
 /**
@@ -61,20 +64,39 @@
         super.tearDown();
     }
 
+    // TODO(kneas): After b/27897448 is fixed, remove method or TODO
+    public void forceDeviceHome() throws RemoteException {
+        // Put device to sleep to go back home
+        if (VERSION.SDK_INT >= 20) {
+            mDevice.pressKeyCode(KeyEvent.KEYCODE_SLEEP);
+        } else {
+            mDevice.sleep();
+        }
+        SystemClock.sleep(mHelper.LONG_TIMEOUT);
+        mDevice.wakeUp();
+    }
+
     // Open Text Components
-    public void openTextComponents(String componentName) {
+    public void openTextComponents(String componentName) throws RemoteException {
+        // TODO(kneas): Remove if statement after b/27897448 is fixed
+        // Needed in case the EditTextTyping tests fails, leaving it in the test with the keyboard
+        // open
+        if (mDevice.getProductName().equals("nemo")) {
+            forceDeviceHome();
+        }
         mHelper.launchUiBench();
         mHelper.openTextInList("Text");
         mHelper.openTextInList(componentName);
     }
 
     // Open EditText Typing
-    public void openEditTextTyping() {
+    public void openEditTextTyping() throws RemoteException {
         openTextComponents("EditText Typing");
     }
 
     // Measure jank metrics for EditText Typing
-    @JankTest(beforeTest="openEditTextTyping", afterTest="goBackHome",
+    // TODO(kneas): Change afterTest to "goBackHome" after b/27897448 is fixed
+    @JankTest(beforeTest="openEditTextTyping", afterTest="goBackHomeEditText",
         expectedFrames=EXPECTED_FRAMES)
     @GfxMonitor(processName=PACKAGE_NAME)
     public void testEditTextTyping() {
@@ -82,7 +104,7 @@
     }
 
     // Open Layout Cache High Hitrate
-    public void openLayoutCacheHighHitrate() {
+    public void openLayoutCacheHighHitrate() throws RemoteException {
         openTextComponents("Layout Cache High Hitrate");
     }
 
@@ -93,7 +115,8 @@
     public void testLayoutCacheHighHitrateFling() {
         UiObject2 layoutCacheHighHitrateContents = mDevice.wait(Until.findObject(
                 By.clazz(ListView.class)), mHelper.TIMEOUT);
-        Assert.assertNotNull("LayoutCacheHighHitrateContents isn't found", layoutCacheHighHitrateContents);
+        Assert.assertNotNull("LayoutCacheHighHitrateContents isn't found",
+                layoutCacheHighHitrateContents);
 
         for (int i = 0; i < mHelper.INNER_LOOP; i++) {
             try {
@@ -114,7 +137,7 @@
     }
 
     // Open Layout Cache Low Hitrate
-    public void openLayoutCacheLowHitrate() {
+    public void openLayoutCacheLowHitrate() throws RemoteException {
         openTextComponents("Layout Cache Low Hitrate");
     }
 
@@ -125,7 +148,8 @@
     public void testLayoutCacheLowHitrateFling() {
         UiObject2 layoutCacheLowHitrateContents = mDevice.wait(Until.findObject(
                 By.clazz(ListView.class)), mHelper.TIMEOUT);
-        Assert.assertNotNull("LayoutCacheLowHitrateContents isn't found", layoutCacheLowHitrateContents);
+        Assert.assertNotNull("LayoutCacheLowHitrateContents isn't found",
+                layoutCacheLowHitrateContents);
 
         for (int i = 0; i < mHelper.INNER_LOOP; i++) {
             try {
@@ -150,4 +174,22 @@
             mHelper.goBackHome();
            super.afterTest(metrics);
     }
+
+    // Workaround for b/27897448 until investigation is complete
+    // TODO(kneas): Remove once b/27897448 is fixed
+    public void goBackHomeEditText(Bundle metrics)
+            throws RemoteException, UiObjectNotFoundException {
+        if (mDevice.getProductName() == "nemo") {
+            forceDeviceHome();
+            super.afterTest(metrics);
+            // Relaunch the app. Ideally we're still in EditText, but it's no longer typing
+            // goBackHome will now be able to use the back button, since the keyboard is hidden
+            SystemClock.sleep(mHelper.SHORT_TIMEOUT + mHelper.SHORT_TIMEOUT);
+            mHelper.launchUiBench();
+            mHelper.goBackHome();
+        }
+        else {
+            goBackHome(metrics);
+        }
+    }
 }
diff --git a/tests/perf/PerformanceLaunch/AndroidManifest.xml b/tests/perf/PerformanceLaunch/AndroidManifest.xml
index d4aca87..48461a2 100644
--- a/tests/perf/PerformanceLaunch/AndroidManifest.xml
+++ b/tests/perf/PerformanceLaunch/AndroidManifest.xml
@@ -74,6 +74,12 @@
             android:autoRemoveFromRecents="true"
             android:exported="true"
             android:screenOrientation="nosensor" />
+        <activity
+            android:name=".ManyConfigResourceActivity"
+            android:label="@string/app_name"
+            android:autoRemoveFromRecents="true"
+            android:exported="true"
+            android:screenOrientation="nosensor" />
     </application>
 
 </manifest>
diff --git a/tests/perf/PerformanceLaunch/gen_locales.py b/tests/perf/PerformanceLaunch/gen_locales.py
new file mode 100644
index 0000000..ade471a
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/gen_locales.py
@@ -0,0 +1,34 @@
+#!/usr/bin/env python
+
+#
+# Generates a series of res/values-{locale}/ directories with a strings.xml
+# file in each containing 4 resources, many_config_1, many_config_2, many_config_3,
+# and many_config_4.
+#
+
+import os
+
+template = """<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-{0}</string>
+    <string name="many_config_2">ManyConfig1-{0}</string>
+    <string name="many_config_3">ManyConfig1-{0}</string>
+    <string name="many_config_4">ManyConfig1-{0}</string>
+</resources>"""
+
+localeStr = "en_US af_ZA am_ET ar_EG bg_BG bn_BD ca_ES cs_CZ da_DK de_DE el_GR en_AU en_GB en_IN es_ES es_US et_EE eu_ES \
+fa_IR fi_FI fr_CA fr_FR gl_ES hi_IN hr_HR hu_HU hy_AM in_ID is_IS it_IT iw_IL ja_JP ka_GE km_KH ko_KR ky_KG lo_LA lt_LT \
+lv_LV km_MH kn_IN mn_MN ml_IN mk_MK mr_IN ms_MY my_MM ne_NP nb_NO nl_NL pl_PL pt_BR pt_PT ro_RO ru_RU si_LK sk_SK sl_SI \
+sr_RS sv_SE sw_TZ ta_IN te_IN th_TH tl_PH tr_TR uk_UA vi_VN zh_CN zh_HK zh_TW zu_ZA en_XA ar_XB"
+
+locales = [locale.replace("_", "-r") for locale in localeStr.split(" ")]
+
+for locale in locales:
+    try:
+        os.mkdir("res/values-{0}".format(locale))
+    except:
+        pass
+
+    with open("res/values-{0}/strings.xml".format(locale), "w") as f:
+        f.write(template.format(locale))
+
diff --git a/tests/perf/PerformanceLaunch/res/values-af-rZA/strings.xml b/tests/perf/PerformanceLaunch/res/values-af-rZA/strings.xml
new file mode 100644
index 0000000..8123d61
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-af-rZA/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-af-rZA</string>
+    <string name="many_config_2">ManyConfig1-af-rZA</string>
+    <string name="many_config_3">ManyConfig1-af-rZA</string>
+    <string name="many_config_4">ManyConfig1-af-rZA</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-am-rET/strings.xml b/tests/perf/PerformanceLaunch/res/values-am-rET/strings.xml
new file mode 100644
index 0000000..ee4fe92
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-am-rET/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-am-rET</string>
+    <string name="many_config_2">ManyConfig1-am-rET</string>
+    <string name="many_config_3">ManyConfig1-am-rET</string>
+    <string name="many_config_4">ManyConfig1-am-rET</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-ar-rEG/strings.xml b/tests/perf/PerformanceLaunch/res/values-ar-rEG/strings.xml
new file mode 100644
index 0000000..fdd7830
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-ar-rEG/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-ar-rEG</string>
+    <string name="many_config_2">ManyConfig1-ar-rEG</string>
+    <string name="many_config_3">ManyConfig1-ar-rEG</string>
+    <string name="many_config_4">ManyConfig1-ar-rEG</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-ar-rXB/strings.xml b/tests/perf/PerformanceLaunch/res/values-ar-rXB/strings.xml
new file mode 100644
index 0000000..8f90e7c
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-ar-rXB/strings.xml
@@ -0,0 +1,7 @@
+<?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-bg-rBG/strings.xml b/tests/perf/PerformanceLaunch/res/values-bg-rBG/strings.xml
new file mode 100644
index 0000000..07823e9
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-bg-rBG/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-bg-rBG</string>
+    <string name="many_config_2">ManyConfig1-bg-rBG</string>
+    <string name="many_config_3">ManyConfig1-bg-rBG</string>
+    <string name="many_config_4">ManyConfig1-bg-rBG</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-bn-rBD/strings.xml b/tests/perf/PerformanceLaunch/res/values-bn-rBD/strings.xml
new file mode 100644
index 0000000..90d3456
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-bn-rBD/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-bn-rBD</string>
+    <string name="many_config_2">ManyConfig1-bn-rBD</string>
+    <string name="many_config_3">ManyConfig1-bn-rBD</string>
+    <string name="many_config_4">ManyConfig1-bn-rBD</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-ca-rES/strings.xml b/tests/perf/PerformanceLaunch/res/values-ca-rES/strings.xml
new file mode 100644
index 0000000..40aad1f
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-ca-rES/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-ca-rES</string>
+    <string name="many_config_2">ManyConfig1-ca-rES</string>
+    <string name="many_config_3">ManyConfig1-ca-rES</string>
+    <string name="many_config_4">ManyConfig1-ca-rES</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-cs-rCZ/strings.xml b/tests/perf/PerformanceLaunch/res/values-cs-rCZ/strings.xml
new file mode 100644
index 0000000..78745a1
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-cs-rCZ/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-cs-rCZ</string>
+    <string name="many_config_2">ManyConfig1-cs-rCZ</string>
+    <string name="many_config_3">ManyConfig1-cs-rCZ</string>
+    <string name="many_config_4">ManyConfig1-cs-rCZ</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-da-rDK/strings.xml b/tests/perf/PerformanceLaunch/res/values-da-rDK/strings.xml
new file mode 100644
index 0000000..4474449
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-da-rDK/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-da-rDK</string>
+    <string name="many_config_2">ManyConfig1-da-rDK</string>
+    <string name="many_config_3">ManyConfig1-da-rDK</string>
+    <string name="many_config_4">ManyConfig1-da-rDK</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-de-rDE/strings.xml b/tests/perf/PerformanceLaunch/res/values-de-rDE/strings.xml
new file mode 100644
index 0000000..3e4eede
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-de-rDE/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-de-rDE</string>
+    <string name="many_config_2">ManyConfig1-de-rDE</string>
+    <string name="many_config_3">ManyConfig1-de-rDE</string>
+    <string name="many_config_4">ManyConfig1-de-rDE</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-el-rGR/strings.xml b/tests/perf/PerformanceLaunch/res/values-el-rGR/strings.xml
new file mode 100644
index 0000000..600cb02
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-el-rGR/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-el-rGR</string>
+    <string name="many_config_2">ManyConfig1-el-rGR</string>
+    <string name="many_config_3">ManyConfig1-el-rGR</string>
+    <string name="many_config_4">ManyConfig1-el-rGR</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-en-rAU/strings.xml b/tests/perf/PerformanceLaunch/res/values-en-rAU/strings.xml
new file mode 100644
index 0000000..28541e3
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-en-rAU/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-en-rAU</string>
+    <string name="many_config_2">ManyConfig1-en-rAU</string>
+    <string name="many_config_3">ManyConfig1-en-rAU</string>
+    <string name="many_config_4">ManyConfig1-en-rAU</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-en-rGB/strings.xml b/tests/perf/PerformanceLaunch/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000..8fa5df5
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-en-rGB/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-en-rGB</string>
+    <string name="many_config_2">ManyConfig1-en-rGB</string>
+    <string name="many_config_3">ManyConfig1-en-rGB</string>
+    <string name="many_config_4">ManyConfig1-en-rGB</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-en-rIN/strings.xml b/tests/perf/PerformanceLaunch/res/values-en-rIN/strings.xml
new file mode 100644
index 0000000..e3ed318
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-en-rIN/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-en-rIN</string>
+    <string name="many_config_2">ManyConfig1-en-rIN</string>
+    <string name="many_config_3">ManyConfig1-en-rIN</string>
+    <string name="many_config_4">ManyConfig1-en-rIN</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-en-rUS/strings.xml b/tests/perf/PerformanceLaunch/res/values-en-rUS/strings.xml
new file mode 100644
index 0000000..aa6e9ab
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-en-rUS/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-en-rUS</string>
+    <string name="many_config_2">ManyConfig1-en-rUS</string>
+    <string name="many_config_3">ManyConfig1-en-rUS</string>
+    <string name="many_config_4">ManyConfig1-en-rUS</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
new file mode 100644
index 0000000..67cf292
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-en-rXA/strings.xml
@@ -0,0 +1,7 @@
+<?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/PerformanceLaunch/res/values-es-rES/strings.xml b/tests/perf/PerformanceLaunch/res/values-es-rES/strings.xml
new file mode 100644
index 0000000..a40dfaf
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-es-rES/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-es-rES</string>
+    <string name="many_config_2">ManyConfig1-es-rES</string>
+    <string name="many_config_3">ManyConfig1-es-rES</string>
+    <string name="many_config_4">ManyConfig1-es-rES</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-es-rUS/strings.xml b/tests/perf/PerformanceLaunch/res/values-es-rUS/strings.xml
new file mode 100644
index 0000000..8003c4a
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-es-rUS/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-es-rUS</string>
+    <string name="many_config_2">ManyConfig1-es-rUS</string>
+    <string name="many_config_3">ManyConfig1-es-rUS</string>
+    <string name="many_config_4">ManyConfig1-es-rUS</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-et-rEE/strings.xml b/tests/perf/PerformanceLaunch/res/values-et-rEE/strings.xml
new file mode 100644
index 0000000..bf3c141
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-et-rEE/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-et-rEE</string>
+    <string name="many_config_2">ManyConfig1-et-rEE</string>
+    <string name="many_config_3">ManyConfig1-et-rEE</string>
+    <string name="many_config_4">ManyConfig1-et-rEE</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-eu-rES/strings.xml b/tests/perf/PerformanceLaunch/res/values-eu-rES/strings.xml
new file mode 100644
index 0000000..4ea21d6
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-eu-rES/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-eu-rES</string>
+    <string name="many_config_2">ManyConfig1-eu-rES</string>
+    <string name="many_config_3">ManyConfig1-eu-rES</string>
+    <string name="many_config_4">ManyConfig1-eu-rES</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-fa-rIR/strings.xml b/tests/perf/PerformanceLaunch/res/values-fa-rIR/strings.xml
new file mode 100644
index 0000000..02156e0
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-fa-rIR/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-fa-rIR</string>
+    <string name="many_config_2">ManyConfig1-fa-rIR</string>
+    <string name="many_config_3">ManyConfig1-fa-rIR</string>
+    <string name="many_config_4">ManyConfig1-fa-rIR</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-fi-rFI/strings.xml b/tests/perf/PerformanceLaunch/res/values-fi-rFI/strings.xml
new file mode 100644
index 0000000..1fe2c98
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-fi-rFI/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-fi-rFI</string>
+    <string name="many_config_2">ManyConfig1-fi-rFI</string>
+    <string name="many_config_3">ManyConfig1-fi-rFI</string>
+    <string name="many_config_4">ManyConfig1-fi-rFI</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-fr-rCA/strings.xml b/tests/perf/PerformanceLaunch/res/values-fr-rCA/strings.xml
new file mode 100644
index 0000000..a1a8bfd
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-fr-rCA/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-fr-rCA</string>
+    <string name="many_config_2">ManyConfig1-fr-rCA</string>
+    <string name="many_config_3">ManyConfig1-fr-rCA</string>
+    <string name="many_config_4">ManyConfig1-fr-rCA</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-fr-rFR/strings.xml b/tests/perf/PerformanceLaunch/res/values-fr-rFR/strings.xml
new file mode 100644
index 0000000..6c3405b
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-fr-rFR/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-fr-rFR</string>
+    <string name="many_config_2">ManyConfig1-fr-rFR</string>
+    <string name="many_config_3">ManyConfig1-fr-rFR</string>
+    <string name="many_config_4">ManyConfig1-fr-rFR</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-gl-rES/strings.xml b/tests/perf/PerformanceLaunch/res/values-gl-rES/strings.xml
new file mode 100644
index 0000000..bbaa529
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-gl-rES/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-gl-rES</string>
+    <string name="many_config_2">ManyConfig1-gl-rES</string>
+    <string name="many_config_3">ManyConfig1-gl-rES</string>
+    <string name="many_config_4">ManyConfig1-gl-rES</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-hi-rIN/strings.xml b/tests/perf/PerformanceLaunch/res/values-hi-rIN/strings.xml
new file mode 100644
index 0000000..bc56098
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-hi-rIN/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-hi-rIN</string>
+    <string name="many_config_2">ManyConfig1-hi-rIN</string>
+    <string name="many_config_3">ManyConfig1-hi-rIN</string>
+    <string name="many_config_4">ManyConfig1-hi-rIN</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-hr-rHR/strings.xml b/tests/perf/PerformanceLaunch/res/values-hr-rHR/strings.xml
new file mode 100644
index 0000000..82afaee
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-hr-rHR/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-hr-rHR</string>
+    <string name="many_config_2">ManyConfig1-hr-rHR</string>
+    <string name="many_config_3">ManyConfig1-hr-rHR</string>
+    <string name="many_config_4">ManyConfig1-hr-rHR</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-hu-rHU/strings.xml b/tests/perf/PerformanceLaunch/res/values-hu-rHU/strings.xml
new file mode 100644
index 0000000..8316f2c
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-hu-rHU/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-hu-rHU</string>
+    <string name="many_config_2">ManyConfig1-hu-rHU</string>
+    <string name="many_config_3">ManyConfig1-hu-rHU</string>
+    <string name="many_config_4">ManyConfig1-hu-rHU</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-hy-rAM/strings.xml b/tests/perf/PerformanceLaunch/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000..cec6901
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-hy-rAM/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-hy-rAM</string>
+    <string name="many_config_2">ManyConfig1-hy-rAM</string>
+    <string name="many_config_3">ManyConfig1-hy-rAM</string>
+    <string name="many_config_4">ManyConfig1-hy-rAM</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-in-rID/strings.xml b/tests/perf/PerformanceLaunch/res/values-in-rID/strings.xml
new file mode 100644
index 0000000..7bceac1
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-in-rID/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-in-rID</string>
+    <string name="many_config_2">ManyConfig1-in-rID</string>
+    <string name="many_config_3">ManyConfig1-in-rID</string>
+    <string name="many_config_4">ManyConfig1-in-rID</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-is-rIS/strings.xml b/tests/perf/PerformanceLaunch/res/values-is-rIS/strings.xml
new file mode 100644
index 0000000..020f94f
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-is-rIS/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-is-rIS</string>
+    <string name="many_config_2">ManyConfig1-is-rIS</string>
+    <string name="many_config_3">ManyConfig1-is-rIS</string>
+    <string name="many_config_4">ManyConfig1-is-rIS</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-it-rIT/strings.xml b/tests/perf/PerformanceLaunch/res/values-it-rIT/strings.xml
new file mode 100644
index 0000000..2116434
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-it-rIT/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-it-rIT</string>
+    <string name="many_config_2">ManyConfig1-it-rIT</string>
+    <string name="many_config_3">ManyConfig1-it-rIT</string>
+    <string name="many_config_4">ManyConfig1-it-rIT</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-iw-rIL/strings.xml b/tests/perf/PerformanceLaunch/res/values-iw-rIL/strings.xml
new file mode 100644
index 0000000..b5a6375
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-iw-rIL/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-iw-rIL</string>
+    <string name="many_config_2">ManyConfig1-iw-rIL</string>
+    <string name="many_config_3">ManyConfig1-iw-rIL</string>
+    <string name="many_config_4">ManyConfig1-iw-rIL</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-ja-rJP/strings.xml b/tests/perf/PerformanceLaunch/res/values-ja-rJP/strings.xml
new file mode 100644
index 0000000..a5ec3e2
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-ja-rJP/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-ja-rJP</string>
+    <string name="many_config_2">ManyConfig1-ja-rJP</string>
+    <string name="many_config_3">ManyConfig1-ja-rJP</string>
+    <string name="many_config_4">ManyConfig1-ja-rJP</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-ka-rGE/strings.xml b/tests/perf/PerformanceLaunch/res/values-ka-rGE/strings.xml
new file mode 100644
index 0000000..8656e74
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-ka-rGE/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-ka-rGE</string>
+    <string name="many_config_2">ManyConfig1-ka-rGE</string>
+    <string name="many_config_3">ManyConfig1-ka-rGE</string>
+    <string name="many_config_4">ManyConfig1-ka-rGE</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-km-rKH/strings.xml b/tests/perf/PerformanceLaunch/res/values-km-rKH/strings.xml
new file mode 100644
index 0000000..188197d
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-km-rKH/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-km-rKH</string>
+    <string name="many_config_2">ManyConfig1-km-rKH</string>
+    <string name="many_config_3">ManyConfig1-km-rKH</string>
+    <string name="many_config_4">ManyConfig1-km-rKH</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-km-rMH/strings.xml b/tests/perf/PerformanceLaunch/res/values-km-rMH/strings.xml
new file mode 100644
index 0000000..fea5400
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-km-rMH/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-km-rMH</string>
+    <string name="many_config_2">ManyConfig1-km-rMH</string>
+    <string name="many_config_3">ManyConfig1-km-rMH</string>
+    <string name="many_config_4">ManyConfig1-km-rMH</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-kn-rIN/strings.xml b/tests/perf/PerformanceLaunch/res/values-kn-rIN/strings.xml
new file mode 100644
index 0000000..3490363
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-kn-rIN/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-kn-rIN</string>
+    <string name="many_config_2">ManyConfig1-kn-rIN</string>
+    <string name="many_config_3">ManyConfig1-kn-rIN</string>
+    <string name="many_config_4">ManyConfig1-kn-rIN</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-ko-rKR/strings.xml b/tests/perf/PerformanceLaunch/res/values-ko-rKR/strings.xml
new file mode 100644
index 0000000..d9fac7d
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-ko-rKR/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-ko-rKR</string>
+    <string name="many_config_2">ManyConfig1-ko-rKR</string>
+    <string name="many_config_3">ManyConfig1-ko-rKR</string>
+    <string name="many_config_4">ManyConfig1-ko-rKR</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-ky-rKG/strings.xml b/tests/perf/PerformanceLaunch/res/values-ky-rKG/strings.xml
new file mode 100644
index 0000000..ec66c79
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-ky-rKG/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-ky-rKG</string>
+    <string name="many_config_2">ManyConfig1-ky-rKG</string>
+    <string name="many_config_3">ManyConfig1-ky-rKG</string>
+    <string name="many_config_4">ManyConfig1-ky-rKG</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-lo-rLA/strings.xml b/tests/perf/PerformanceLaunch/res/values-lo-rLA/strings.xml
new file mode 100644
index 0000000..94d676c
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-lo-rLA/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-lo-rLA</string>
+    <string name="many_config_2">ManyConfig1-lo-rLA</string>
+    <string name="many_config_3">ManyConfig1-lo-rLA</string>
+    <string name="many_config_4">ManyConfig1-lo-rLA</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-lt-rLT/strings.xml b/tests/perf/PerformanceLaunch/res/values-lt-rLT/strings.xml
new file mode 100644
index 0000000..7659da8
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-lt-rLT/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-lt-rLT</string>
+    <string name="many_config_2">ManyConfig1-lt-rLT</string>
+    <string name="many_config_3">ManyConfig1-lt-rLT</string>
+    <string name="many_config_4">ManyConfig1-lt-rLT</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-lv-rLV/strings.xml b/tests/perf/PerformanceLaunch/res/values-lv-rLV/strings.xml
new file mode 100644
index 0000000..cf2f3bb
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-lv-rLV/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-lv-rLV</string>
+    <string name="many_config_2">ManyConfig1-lv-rLV</string>
+    <string name="many_config_3">ManyConfig1-lv-rLV</string>
+    <string name="many_config_4">ManyConfig1-lv-rLV</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-mk-rMK/strings.xml b/tests/perf/PerformanceLaunch/res/values-mk-rMK/strings.xml
new file mode 100644
index 0000000..9e65f29
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-mk-rMK/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-mk-rMK</string>
+    <string name="many_config_2">ManyConfig1-mk-rMK</string>
+    <string name="many_config_3">ManyConfig1-mk-rMK</string>
+    <string name="many_config_4">ManyConfig1-mk-rMK</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-ml-rIN/strings.xml b/tests/perf/PerformanceLaunch/res/values-ml-rIN/strings.xml
new file mode 100644
index 0000000..c5aede0
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-ml-rIN/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-ml-rIN</string>
+    <string name="many_config_2">ManyConfig1-ml-rIN</string>
+    <string name="many_config_3">ManyConfig1-ml-rIN</string>
+    <string name="many_config_4">ManyConfig1-ml-rIN</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-mn-rMN/strings.xml b/tests/perf/PerformanceLaunch/res/values-mn-rMN/strings.xml
new file mode 100644
index 0000000..2d369f0
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-mn-rMN/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-mn-rMN</string>
+    <string name="many_config_2">ManyConfig1-mn-rMN</string>
+    <string name="many_config_3">ManyConfig1-mn-rMN</string>
+    <string name="many_config_4">ManyConfig1-mn-rMN</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-mr-rIN/strings.xml b/tests/perf/PerformanceLaunch/res/values-mr-rIN/strings.xml
new file mode 100644
index 0000000..63517b8
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-mr-rIN/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-mr-rIN</string>
+    <string name="many_config_2">ManyConfig1-mr-rIN</string>
+    <string name="many_config_3">ManyConfig1-mr-rIN</string>
+    <string name="many_config_4">ManyConfig1-mr-rIN</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-ms-rMY/strings.xml b/tests/perf/PerformanceLaunch/res/values-ms-rMY/strings.xml
new file mode 100644
index 0000000..4d2b27b
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-ms-rMY/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-ms-rMY</string>
+    <string name="many_config_2">ManyConfig1-ms-rMY</string>
+    <string name="many_config_3">ManyConfig1-ms-rMY</string>
+    <string name="many_config_4">ManyConfig1-ms-rMY</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-my-rMM/strings.xml b/tests/perf/PerformanceLaunch/res/values-my-rMM/strings.xml
new file mode 100644
index 0000000..1949158
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-my-rMM/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-my-rMM</string>
+    <string name="many_config_2">ManyConfig1-my-rMM</string>
+    <string name="many_config_3">ManyConfig1-my-rMM</string>
+    <string name="many_config_4">ManyConfig1-my-rMM</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-nb-rNO/strings.xml b/tests/perf/PerformanceLaunch/res/values-nb-rNO/strings.xml
new file mode 100644
index 0000000..1f2eaa7
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-nb-rNO/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-nb-rNO</string>
+    <string name="many_config_2">ManyConfig1-nb-rNO</string>
+    <string name="many_config_3">ManyConfig1-nb-rNO</string>
+    <string name="many_config_4">ManyConfig1-nb-rNO</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-ne-rNP/strings.xml b/tests/perf/PerformanceLaunch/res/values-ne-rNP/strings.xml
new file mode 100644
index 0000000..fa1d743
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-ne-rNP/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-ne-rNP</string>
+    <string name="many_config_2">ManyConfig1-ne-rNP</string>
+    <string name="many_config_3">ManyConfig1-ne-rNP</string>
+    <string name="many_config_4">ManyConfig1-ne-rNP</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-nl-rNL/strings.xml b/tests/perf/PerformanceLaunch/res/values-nl-rNL/strings.xml
new file mode 100644
index 0000000..440f648
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-nl-rNL/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-nl-rNL</string>
+    <string name="many_config_2">ManyConfig1-nl-rNL</string>
+    <string name="many_config_3">ManyConfig1-nl-rNL</string>
+    <string name="many_config_4">ManyConfig1-nl-rNL</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-pl-rPL/strings.xml b/tests/perf/PerformanceLaunch/res/values-pl-rPL/strings.xml
new file mode 100644
index 0000000..4920307
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-pl-rPL/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-pl-rPL</string>
+    <string name="many_config_2">ManyConfig1-pl-rPL</string>
+    <string name="many_config_3">ManyConfig1-pl-rPL</string>
+    <string name="many_config_4">ManyConfig1-pl-rPL</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-pt-rBR/strings.xml b/tests/perf/PerformanceLaunch/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000..cb8d260
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-pt-rBR/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-pt-rBR</string>
+    <string name="many_config_2">ManyConfig1-pt-rBR</string>
+    <string name="many_config_3">ManyConfig1-pt-rBR</string>
+    <string name="many_config_4">ManyConfig1-pt-rBR</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-pt-rPT/strings.xml b/tests/perf/PerformanceLaunch/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000..c74ad76
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-pt-rPT/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-pt-rPT</string>
+    <string name="many_config_2">ManyConfig1-pt-rPT</string>
+    <string name="many_config_3">ManyConfig1-pt-rPT</string>
+    <string name="many_config_4">ManyConfig1-pt-rPT</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-ro-rRO/strings.xml b/tests/perf/PerformanceLaunch/res/values-ro-rRO/strings.xml
new file mode 100644
index 0000000..08699c5
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-ro-rRO/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-ro-rRO</string>
+    <string name="many_config_2">ManyConfig1-ro-rRO</string>
+    <string name="many_config_3">ManyConfig1-ro-rRO</string>
+    <string name="many_config_4">ManyConfig1-ro-rRO</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-ru-rRU/strings.xml b/tests/perf/PerformanceLaunch/res/values-ru-rRU/strings.xml
new file mode 100644
index 0000000..afe80bb
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-ru-rRU/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-ru-rRU</string>
+    <string name="many_config_2">ManyConfig1-ru-rRU</string>
+    <string name="many_config_3">ManyConfig1-ru-rRU</string>
+    <string name="many_config_4">ManyConfig1-ru-rRU</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-si-rLK/strings.xml b/tests/perf/PerformanceLaunch/res/values-si-rLK/strings.xml
new file mode 100644
index 0000000..5e9d6a4
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-si-rLK/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-si-rLK</string>
+    <string name="many_config_2">ManyConfig1-si-rLK</string>
+    <string name="many_config_3">ManyConfig1-si-rLK</string>
+    <string name="many_config_4">ManyConfig1-si-rLK</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-sk-rSK/strings.xml b/tests/perf/PerformanceLaunch/res/values-sk-rSK/strings.xml
new file mode 100644
index 0000000..4a9ad58
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-sk-rSK/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-sk-rSK</string>
+    <string name="many_config_2">ManyConfig1-sk-rSK</string>
+    <string name="many_config_3">ManyConfig1-sk-rSK</string>
+    <string name="many_config_4">ManyConfig1-sk-rSK</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-sl-rSI/strings.xml b/tests/perf/PerformanceLaunch/res/values-sl-rSI/strings.xml
new file mode 100644
index 0000000..41afd9d
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-sl-rSI/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-sl-rSI</string>
+    <string name="many_config_2">ManyConfig1-sl-rSI</string>
+    <string name="many_config_3">ManyConfig1-sl-rSI</string>
+    <string name="many_config_4">ManyConfig1-sl-rSI</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-sr-rRS/strings.xml b/tests/perf/PerformanceLaunch/res/values-sr-rRS/strings.xml
new file mode 100644
index 0000000..d762a50
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-sr-rRS/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-sr-rRS</string>
+    <string name="many_config_2">ManyConfig1-sr-rRS</string>
+    <string name="many_config_3">ManyConfig1-sr-rRS</string>
+    <string name="many_config_4">ManyConfig1-sr-rRS</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-sv-rSE/strings.xml b/tests/perf/PerformanceLaunch/res/values-sv-rSE/strings.xml
new file mode 100644
index 0000000..5dbf904
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-sv-rSE/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-sv-rSE</string>
+    <string name="many_config_2">ManyConfig1-sv-rSE</string>
+    <string name="many_config_3">ManyConfig1-sv-rSE</string>
+    <string name="many_config_4">ManyConfig1-sv-rSE</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-sw-rTZ/strings.xml b/tests/perf/PerformanceLaunch/res/values-sw-rTZ/strings.xml
new file mode 100644
index 0000000..189ce86
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-sw-rTZ/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-sw-rTZ</string>
+    <string name="many_config_2">ManyConfig1-sw-rTZ</string>
+    <string name="many_config_3">ManyConfig1-sw-rTZ</string>
+    <string name="many_config_4">ManyConfig1-sw-rTZ</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-ta-rIN/strings.xml b/tests/perf/PerformanceLaunch/res/values-ta-rIN/strings.xml
new file mode 100644
index 0000000..ded7c28
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-ta-rIN/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-ta-rIN</string>
+    <string name="many_config_2">ManyConfig1-ta-rIN</string>
+    <string name="many_config_3">ManyConfig1-ta-rIN</string>
+    <string name="many_config_4">ManyConfig1-ta-rIN</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-te-rIN/strings.xml b/tests/perf/PerformanceLaunch/res/values-te-rIN/strings.xml
new file mode 100644
index 0000000..bd442e9
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-te-rIN/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-te-rIN</string>
+    <string name="many_config_2">ManyConfig1-te-rIN</string>
+    <string name="many_config_3">ManyConfig1-te-rIN</string>
+    <string name="many_config_4">ManyConfig1-te-rIN</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-th-rTH/strings.xml b/tests/perf/PerformanceLaunch/res/values-th-rTH/strings.xml
new file mode 100644
index 0000000..1fcb347
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-th-rTH/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-th-rTH</string>
+    <string name="many_config_2">ManyConfig1-th-rTH</string>
+    <string name="many_config_3">ManyConfig1-th-rTH</string>
+    <string name="many_config_4">ManyConfig1-th-rTH</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-tl-rPH/strings.xml b/tests/perf/PerformanceLaunch/res/values-tl-rPH/strings.xml
new file mode 100644
index 0000000..5d455f4
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-tl-rPH/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-tl-rPH</string>
+    <string name="many_config_2">ManyConfig1-tl-rPH</string>
+    <string name="many_config_3">ManyConfig1-tl-rPH</string>
+    <string name="many_config_4">ManyConfig1-tl-rPH</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-tr-rTR/strings.xml b/tests/perf/PerformanceLaunch/res/values-tr-rTR/strings.xml
new file mode 100644
index 0000000..1516193
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-tr-rTR/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-tr-rTR</string>
+    <string name="many_config_2">ManyConfig1-tr-rTR</string>
+    <string name="many_config_3">ManyConfig1-tr-rTR</string>
+    <string name="many_config_4">ManyConfig1-tr-rTR</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-uk-rUA/strings.xml b/tests/perf/PerformanceLaunch/res/values-uk-rUA/strings.xml
new file mode 100644
index 0000000..a2d1dfe
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-uk-rUA/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-uk-rUA</string>
+    <string name="many_config_2">ManyConfig1-uk-rUA</string>
+    <string name="many_config_3">ManyConfig1-uk-rUA</string>
+    <string name="many_config_4">ManyConfig1-uk-rUA</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-v11/styles.xml b/tests/perf/PerformanceLaunch/res/values-v11/styles.xml
deleted file mode 100644
index 3c02242..0000000
--- a/tests/perf/PerformanceLaunch/res/values-v11/styles.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-<resources>
-
-    <!--
-        Base application theme for API 11+. This theme completely replaces
-        AppBaseTheme from res/values/styles.xml on API 11+ devices.
-    -->
-    <style name="AppBaseTheme" parent="android:Theme.Holo.Light">
-        <!-- API 11 theme customizations can go here. -->
-    </style>
-
-</resources>
diff --git a/tests/perf/PerformanceLaunch/res/values-v14/styles.xml b/tests/perf/PerformanceLaunch/res/values-v14/styles.xml
deleted file mode 100644
index a91fd03..0000000
--- a/tests/perf/PerformanceLaunch/res/values-v14/styles.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-<resources>
-
-    <!--
-        Base application theme for API 14+. This theme completely replaces
-        AppBaseTheme from BOTH res/values/styles.xml and
-        res/values-v11/styles.xml on API 14+ devices.
-    -->
-    <style name="AppBaseTheme" parent="android:Theme.Holo.Light.DarkActionBar">
-        <!-- API 14 theme customizations can go here. -->
-    </style>
-
-</resources>
diff --git a/tests/perf/PerformanceLaunch/res/values-v21/styles.xml b/tests/perf/PerformanceLaunch/res/values-v21/styles.xml
new file mode 100644
index 0000000..ea65cee
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-v21/styles.xml
@@ -0,0 +1,4 @@
+<resources>
+    <style name="AppBaseTheme" parent="android:Theme.Material.Light">
+    </style>
+</resources>
diff --git a/tests/perf/PerformanceLaunch/res/values-vi-rVN/strings.xml b/tests/perf/PerformanceLaunch/res/values-vi-rVN/strings.xml
new file mode 100644
index 0000000..a5d9543
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-vi-rVN/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-vi-rVN</string>
+    <string name="many_config_2">ManyConfig1-vi-rVN</string>
+    <string name="many_config_3">ManyConfig1-vi-rVN</string>
+    <string name="many_config_4">ManyConfig1-vi-rVN</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-zh-rCN/strings.xml b/tests/perf/PerformanceLaunch/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000..f48c4d9
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-zh-rCN/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-zh-rCN</string>
+    <string name="many_config_2">ManyConfig1-zh-rCN</string>
+    <string name="many_config_3">ManyConfig1-zh-rCN</string>
+    <string name="many_config_4">ManyConfig1-zh-rCN</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-zh-rHK/strings.xml b/tests/perf/PerformanceLaunch/res/values-zh-rHK/strings.xml
new file mode 100644
index 0000000..6b043a8
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-zh-rHK/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-zh-rHK</string>
+    <string name="many_config_2">ManyConfig1-zh-rHK</string>
+    <string name="many_config_3">ManyConfig1-zh-rHK</string>
+    <string name="many_config_4">ManyConfig1-zh-rHK</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-zh-rTW/strings.xml b/tests/perf/PerformanceLaunch/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000..6992c18
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-zh-rTW/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-zh-rTW</string>
+    <string name="many_config_2">ManyConfig1-zh-rTW</string>
+    <string name="many_config_3">ManyConfig1-zh-rTW</string>
+    <string name="many_config_4">ManyConfig1-zh-rTW</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-zu-rZA/strings.xml b/tests/perf/PerformanceLaunch/res/values-zu-rZA/strings.xml
new file mode 100644
index 0000000..35ec37b
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-zu-rZA/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="many_config_1">ManyConfig1-zu-rZA</string>
+    <string name="many_config_2">ManyConfig1-zu-rZA</string>
+    <string name="many_config_3">ManyConfig1-zu-rZA</string>
+    <string name="many_config_4">ManyConfig1-zu-rZA</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values/strings.xml b/tests/perf/PerformanceLaunch/res/values/strings.xml
index 59b0466..93f5a3e 100644
--- a/tests/perf/PerformanceLaunch/res/values/strings.xml
+++ b/tests/perf/PerformanceLaunch/res/values/strings.xml
@@ -7,4 +7,17 @@
     <string name="pass">Password:</string>
     <string name="Login">Sign In</string>
 
-</resources>
\ No newline at end of file
+    <!-- Used in ManyConfigResourceActivity -->
+
+    <string-array name="many_configs">
+        <item>@string/many_config_1</item>
+        <item>@string/many_config_2</item>
+        <item>@string/many_config_3</item>
+        <item>@string/many_config_4</item>
+    </string-array>
+
+    <string name="many_config_1">ManyConfig1</string>
+    <string name="many_config_2">ManyConfig2</string>
+    <string name="many_config_3">ManyConfig3</string>
+    <string name="many_config_4">ManyConfig4</string>
+</resources>
diff --git a/tests/perf/PerformanceLaunch/src/com/android/performanceLaunch/ManyConfigResourceActivity.java b/tests/perf/PerformanceLaunch/src/com/android/performanceLaunch/ManyConfigResourceActivity.java
new file mode 100644
index 0000000..d82457a
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/src/com/android/performanceLaunch/ManyConfigResourceActivity.java
@@ -0,0 +1,31 @@
+/*
+ * 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.performanceLaunch;
+
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.os.Bundle;
+
+public class ManyConfigResourceActivity extends Activity {
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        for (int i = 0; i < 1000; i++) {
+            getResources().getStringArray(R.array.many_configs);
+        }
+    }
+}
diff --git a/tests/smokefast/.gitignore b/tests/smokefast/.gitignore
new file mode 100644
index 0000000..77120d7
--- /dev/null
+++ b/tests/smokefast/.gitignore
@@ -0,0 +1,5 @@
+bin/
+gen/
+.settings/
+.classpath
+.project
diff --git a/tests/smokefast/Android.mk b/tests/smokefast/Android.mk
new file mode 100644
index 0000000..93910c6
--- /dev/null
+++ b/tests/smokefast/Android.mk
@@ -0,0 +1,18 @@
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_SDK_VERSION := current
+
+media_framework_app_base := frameworks/base/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest
+
+LOCAL_SRC_FILES := $(call all-subdir-java-files)
+LOCAL_JAVA_LIBRARIES := android.test.runner
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-test ub-uiautomator
+# TODO: AuptLib and aupt-helpers should be static deps as well
+
+LOCAL_PACKAGE_NAME := SmokeFastTests
+LOCAL_CERTIFICATE := platform
+
+include $(BUILD_PACKAGE)
diff --git a/tests/smokefast/AndroidManifest.xml b/tests/smokefast/AndroidManifest.xml
new file mode 100644
index 0000000..a2ed58f
--- /dev/null
+++ b/tests/smokefast/AndroidManifest.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 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.smokefast">
+
+    <uses-sdk android:minSdkVersion="23"
+              android:targetSdkVersion="23" />
+    <uses-feature android:name="android.hardware.camera"
+                  android:required="true" />
+    <uses-feature android:name="android.hardware.telephony"
+                  android:required="false" />
+    <uses-permission android:name="android.permission.CAMERA" />
+    <uses-permission android:name="android.permission.WAKE_LOCK" />
+    <uses-permission android:name="android.permission.SEND_SMS" />
+    <uses-permission android:name="android.permission.CAPTURE_VIDEO_OUTPUT" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.CALL_PHONE" />
+    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.GET_ACCOUNTS" />
+    <application>
+        <uses-library android:name="android.test.runner" />
+        <activity android:label="MediaPlaybackTest"
+                android:name=".app.MediaPlaybackTestApp"
+                android:screenOrientation="landscape">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+    <instrumentation
+            android:name="android.test.InstrumentationTestRunner"
+            android:targetPackage="com.android.smokefast"
+            android:label="SmokeFAST Tests" />
+</manifest>
diff --git a/tests/smokefast/project.properties b/tests/smokefast/project.properties
new file mode 100644
index 0000000..8451e89
--- /dev/null
+++ b/tests/smokefast/project.properties
@@ -0,0 +1 @@
+target=android-23
diff --git a/tests/smokefast/res/layout/surface_view.xml b/tests/smokefast/res/layout/surface_view.xml
new file mode 100644
index 0000000..4999e5d
--- /dev/null
+++ b/tests/smokefast/res/layout/surface_view.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+  android:layout_width="match_parent"
+  android:layout_height="match_parent"
+  android:orientation="vertical">
+
+  <FrameLayout
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+  <SurfaceView
+     android:id="@+id/surface_view"
+     android:layout_width="match_parent"
+     android:layout_height="match_parent"
+     android:layout_centerInParent="true"
+     />
+
+  <ImageView android:id="@+id/overlay_layer"
+     android:layout_width="0dip"
+     android:layout_height="392dip"/>
+
+  <VideoView
+   android:id="@+id/video_view"
+        android:layout_width="320px"
+        android:layout_height="240px"
+  />
+
+  </FrameLayout>
+
+</LinearLayout>
+
diff --git a/tests/smokefast/res/raw/bbb.mkv b/tests/smokefast/res/raw/bbb.mkv
new file mode 100644
index 0000000..e286e01
--- /dev/null
+++ b/tests/smokefast/res/raw/bbb.mkv
Binary files differ
diff --git a/tests/smokefast/src/com/android/smokefast/LockscreenTest.java b/tests/smokefast/src/com/android/smokefast/LockscreenTest.java
new file mode 100644
index 0000000..6a8dba6
--- /dev/null
+++ b/tests/smokefast/src/com/android/smokefast/LockscreenTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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.smokefast;
+
+import android.os.SystemClock;
+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.LargeTest;
+
+public class LockscreenTest extends InstrumentationTestCase {
+    private static final String LAUNCHER_PACKAGE = "com.google.android.googlequicksearchbox";
+    private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
+    private UiDevice mDevice;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mDevice = UiDevice.getInstance(getInstrumentation());
+        mDevice.freezeRotation();
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        mDevice.wakeUp();
+        mDevice.pressMenu();
+        mDevice.unfreezeRotation();
+        super.tearDown();
+    }
+
+    @LargeTest
+    public void testSlideUnlock() throws Exception {
+        sleepAndWakeUpDevice();
+        mDevice.wait(Until.findObject(
+                By.res(SYSTEMUI_PACKAGE, "notification_stack_scroller")), 2000)
+                .swipe(Direction.UP, 1.0f);
+        int counter = 6;
+        UiObject2 workspace =  mDevice.findObject(By.res(LAUNCHER_PACKAGE, "workspace"));
+        while (counter-- > 0 && workspace == null) {
+            workspace =  mDevice.findObject(By.res(LAUNCHER_PACKAGE, "workspace"));
+            SystemClock.sleep(500);
+        }
+        assertNotNull("Workspace wasn't found", workspace);
+    }
+
+    private void sleepAndWakeUpDevice() throws Exception {
+        mDevice.sleep();
+        SystemClock.sleep(1000);
+        mDevice.wakeUp();
+    }
+}
diff --git a/tests/smokefast/src/com/android/smokefast/MediaCaptureTest.java b/tests/smokefast/src/com/android/smokefast/MediaCaptureTest.java
new file mode 100644
index 0000000..e7b3798
--- /dev/null
+++ b/tests/smokefast/src/com/android/smokefast/MediaCaptureTest.java
@@ -0,0 +1,142 @@
+/*
+ * 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.smokefast;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.MediaStore;
+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.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+
+import java.io.File;
+import java.util.regex.Pattern;
+
+/**
+ * Basic tests for the Camera app.
+ */
+public class MediaCaptureTest extends InstrumentationTestCase {
+    private static final int CAPTURE_TIMEOUT = 6000;
+    private static final String DESC_BTN_CAPTURE_PHOTO = "Capture photo";
+    private static final String DESC_BTN_CAPTURE_VIDEO = "Capture video";
+    private static final String DESC_BTN_DONE = "Done";
+    private static final String DESC_BTN_PHOTO_MODE = "Open photo mode";
+    private static final String DESC_BTN_VIDEO_MODE = "Open video mode";
+    private static final int FILE_CHECK_ATTEMPTS = 5;
+    private static final int VIDEO_LENGTH = 2000;
+
+    private UiDevice mDevice;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mDevice = UiDevice.getInstance(getInstrumentation());
+        mDevice.freezeRotation();
+        // if there are any dialogues that pop up, dismiss them
+        UiObject2 maybeOkButton = mDevice.wait(Until.findObject(By.res("android:id/ok_button")),
+                CAPTURE_TIMEOUT);
+        if (maybeOkButton != null) {
+            maybeOkButton.click();
+        }
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        mDevice.unfreezeRotation();
+        super.tearDown();
+    }
+
+    /**
+     * Test that the device can capture a photo.
+     */
+    @LargeTest
+    public void testPhotoCapture() {
+        runCaptureTest(new Intent(MediaStore.ACTION_IMAGE_CAPTURE), "smoke.jpg", false);
+    }
+
+    /**
+     * Test that the device can capture a video.
+     */
+    @LargeTest
+    public void testVideoCapture() {
+        runCaptureTest(new Intent(MediaStore.ACTION_VIDEO_CAPTURE), "smoke.avi", true);
+    }
+
+    private void runCaptureTest(Intent intent, String tmpName, boolean isVideo) {
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        if (intent.resolveActivity(
+                    getInstrumentation().getContext().getPackageManager()) != null) {
+            File outputFile = null;
+            try {
+                outputFile = new File(Environment
+                        .getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM), tmpName);
+                intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(outputFile));
+                getInstrumentation().getContext().startActivity(intent);
+                switchCaptureMode(isVideo);
+                pressCaptureButton(isVideo);
+                if (isVideo) {
+                    Thread.sleep(VIDEO_LENGTH);
+                    pressCaptureButton(isVideo);
+                }
+                Thread.sleep(1000);
+                pushButton(DESC_BTN_DONE);
+                long fileLength = outputFile.length();
+                for (int i=0; i<FILE_CHECK_ATTEMPTS; i++) {
+                    if ((fileLength = outputFile.length()) > 0) {
+                        break;
+                    }
+                    Thread.sleep(1000);
+                }
+                assertTrue(fileLength > 0);
+            } catch (InterruptedException e) {
+                fail(e.getLocalizedMessage());
+            } finally {
+                if (outputFile != null) {
+                    outputFile.delete();
+                }
+            }
+        }
+    }
+
+    private void switchCaptureMode(boolean isVideo) {
+        if (isVideo) {
+            pushButton(DESC_BTN_VIDEO_MODE);
+        } else {
+            pushButton(DESC_BTN_PHOTO_MODE);
+        }
+    }
+
+    private void pressCaptureButton(boolean isVideo) {
+        if (isVideo) {
+            pushButton(DESC_BTN_CAPTURE_VIDEO);
+        } else {
+            pushButton(DESC_BTN_CAPTURE_PHOTO);
+        }
+    }
+
+    private void pushButton(String desc) {
+        Pattern pattern = Pattern.compile(desc, Pattern.CASE_INSENSITIVE);
+        UiObject2 doneBtn = mDevice.wait(Until.findObject(By.desc(pattern)), CAPTURE_TIMEOUT);
+        if (null != doneBtn) {
+            doneBtn.clickAndWait(Until.newWindow(), 500);
+        }
+    }
+}
diff --git a/tests/smokefast/src/com/android/smokefast/MediaPlaybackTest.java b/tests/smokefast/src/com/android/smokefast/MediaPlaybackTest.java
new file mode 100644
index 0000000..156b65e
--- /dev/null
+++ b/tests/smokefast/src/com/android/smokefast/MediaPlaybackTest.java
@@ -0,0 +1,109 @@
+/*
+ * 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.smokefast;
+
+import android.media.MediaPlayer;
+import android.os.Looper;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+
+import com.android.smokefast.app.MediaPlaybackTestApp;
+
+/**
+ * Basic tests for video playback
+ */
+public class MediaPlaybackTest extends ActivityInstrumentationTestCase2<MediaPlaybackTestApp> {
+
+    private static final String TAG = "MediaPlaybackTest";
+    private static final int LOOP_START_BUFFER_MS = 10000;
+    private static final int PLAY_BUFFER_MS = 2000;
+    private final Object mCompletionLock = new Object();
+    private final Object mLooperLock = new Object();
+    private boolean mPlaybackSucceeded = false;
+    private boolean mPlaybackError = false;
+    private Looper mLooper;
+    private MediaPlayer mPlayer;
+
+    public MediaPlaybackTest() {
+        super(MediaPlaybackTestApp.class);
+    }
+
+    @Override
+    public void setUp() throws Exception {
+        // start activity
+        getActivity();
+    }
+
+    @LargeTest
+    public void testVideoPlayback() {
+        // start the MediaPlayer on a Looper thread, so it does not deadlock itself
+        new Thread() {
+            @Override
+            public void run() {
+                Looper.prepare();
+                mLooper = Looper.myLooper();
+                mPlayer = MediaPlayer.create(getInstrumentation().getContext(), R.raw.bbb);
+                mPlayer.setDisplay(getActivity().getSurfaceHolder());
+                synchronized (mLooperLock) {
+                    mLooperLock.notify();
+                }
+                Looper.loop();
+            }
+        }.start();
+        // make sure the looper is really started before we proceed
+        synchronized (mLooperLock) {
+            try {
+                mLooperLock.wait(LOOP_START_BUFFER_MS);
+            } catch (InterruptedException e) {
+                fail("Loop thread start was interrupted");
+            }
+        }
+        mPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
+            @Override
+            public boolean onError(MediaPlayer mp, int what, int extra) {
+                mPlaybackError = true;
+                mp.reset();
+                return true;
+            }
+        });
+        mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
+            @Override
+            public void onCompletion(MediaPlayer mp) {
+                synchronized (mCompletionLock) {
+                    Log.w(TAG, "Hit onCompletion!");
+                    mPlaybackSucceeded = true;
+                    mCompletionLock.notifyAll();
+                }
+            }
+        });
+        mPlayer.start();
+        int duration = mPlayer.getDuration();
+        int currentPosition = mPlayer.getCurrentPosition();
+        synchronized (mCompletionLock) {
+            try {
+                mCompletionLock.wait(duration - currentPosition + PLAY_BUFFER_MS);
+            } catch (InterruptedException e) {
+                fail("Wait for playback was interrupted");
+            }
+        }
+        mLooper.quit();
+        mPlayer.release();
+        assertFalse(mPlaybackError);
+        assertTrue(mPlaybackSucceeded);
+    }
+}
diff --git a/tests/smokefast/src/com/android/smokefast/app/MediaPlaybackTestApp.java b/tests/smokefast/src/com/android/smokefast/app/MediaPlaybackTestApp.java
new file mode 100644
index 0000000..92cd04f
--- /dev/null
+++ b/tests/smokefast/src/com/android/smokefast/app/MediaPlaybackTestApp.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 com.android.smokefast.app;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+
+import com.android.smokefast.R;
+
+public class MediaPlaybackTestApp extends Activity {
+
+    private SurfaceView mSurfaceView;
+
+    @Override
+    public void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+        setContentView(R.layout.surface_view);
+        mSurfaceView = (SurfaceView)findViewById(R.id.surface_view);
+    }
+
+    public SurfaceHolder getSurfaceHolder() {
+        return mSurfaceView.getHolder();
+    }
+}
diff --git a/utils/crashcollector/Android.mk b/utils/crashcollector/Android.mk
new file mode 100644
index 0000000..631ff61
--- /dev/null
+++ b/utils/crashcollector/Android.mk
@@ -0,0 +1,34 @@
+#
+# 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.
+#
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := crashcollector
+LOCAL_MODULE_PATH := $(TARGET_OUT_DATA)/local/tmp/crashcollector
+LOCAL_MODULE_TAGS := optional
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_JAVA_LIBRARY)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := crashcollector
+LOCAL_MODULE_CLASS := EXECUTABLES
+LOCAL_MODULE_TAGS := optional
+LOCAL_MODULE_PATH := $(TARGET_OUT_DATA)/local/tmp/crashcollector
+LOCAL_SRC_FILES := crashcollector
+
+include $(BUILD_PREBUILT)
diff --git a/utils/crashcollector/crashcollector b/utils/crashcollector/crashcollector
new file mode 100755
index 0000000..45c58fb
--- /dev/null
+++ b/utils/crashcollector/crashcollector
@@ -0,0 +1,19 @@
+#
+# 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.
+#
+
+base=/data/local/tmp/crashcollector
+export CLASSPATH=$base/crashcollector.jar
+exec app_process $base android.test.crashcollector.Collector $*
diff --git a/utils/crashcollector/src/android/test/crashcollector/Collector.java b/utils/crashcollector/src/android/test/crashcollector/Collector.java
new file mode 100644
index 0000000..515b73b
--- /dev/null
+++ b/utils/crashcollector/src/android/test/crashcollector/Collector.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright 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.crashcollector;
+
+import android.app.ActivityManagerNative;
+import android.app.IActivityController;
+import android.app.IActivityManager;
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.IBinder.DeathRecipient;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.util.Log;
+
+import java.io.File;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashSet;
+
+/**
+ * Main class for the crash collector that installs an activity controller to monitor app errors
+ */
+public class Collector {
+
+    private static final String LOG_TAG = "CrashCollector";
+    private static final long CHECK_AM_INTERVAL_MS = 5 * 1000;
+    private static final long MAX_CHECK_AM_TIMEOUT_MS = 30 * 1000;
+    private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("yyyy-MM-dd-HH.mm.ss");
+    private static final File TOMBSTONES_PATH = new File("/data/tombstones");
+    private HashSet<String> mTombstones = null;
+
+    /**
+     * Command-line entry point.
+     *
+     * @param args The command-line arguments
+     */
+    public static void main(String[] args) {
+        // Set the process name showing in "ps" or "top"
+        Process.setArgV0("android.test.crashcollector");
+
+        int resultCode = (new Collector()).run(args);
+        System.exit(resultCode);
+    }
+
+    /**
+     * Command execution entry point
+     * @param args
+     * @return
+     * @throws RemoteException
+     */
+    public int run(String[] args) {
+        // recipient for activity manager death so that command can survive runtime restart
+        final IBinder.DeathRecipient death = new DeathRecipient() {
+            @Override
+            public void binderDied() {
+                synchronized (this) {
+                    notifyAll();
+                }
+            }
+        };
+        IBinder am = blockUntilSystemRunning(MAX_CHECK_AM_TIMEOUT_MS);
+        if (am == null) {
+            print("FATAL: Cannot get activity manager, is system running?");
+            return -1;
+        }
+        IActivityController controller = new CrashCollector();
+        do {
+            try {
+                // set activity controller
+                IActivityManager iam = ActivityManagerNative.asInterface(am);
+                iam.setActivityController(controller, false);
+                // register death recipient for activity manager
+                am.linkToDeath(death, 0);
+            } catch (RemoteException re) {
+                print("FATAL: cannot set activity controller, is system running?");
+                re.printStackTrace();
+                return -1;
+            }
+            // monitor runtime restart (crash/kill of system server)
+            synchronized (death) {
+                while (am.isBinderAlive()) {
+                    try {
+                        Log.d(LOG_TAG, "Monitoring death of system server.");
+                        death.wait();
+                    } catch (InterruptedException e) {
+                        // ignore
+                    }
+                }
+                Log.w(LOG_TAG, "Detected crash of system server.");
+                am = blockUntilSystemRunning(MAX_CHECK_AM_TIMEOUT_MS);
+            }
+        } while (true);
+        // for now running indefinitely, until a better mechanism is found to signal shutdown
+    }
+
+    private void print(String line) {
+        System.err.println(String.format("%s %s", TIME_FORMAT.format(new Date()), line));
+    }
+
+    /**
+     * Blocks until system server is running, or timeout has reached
+     * @param timeout
+     * @return
+     */
+    private IBinder blockUntilSystemRunning(long timeout) {
+        // waiting for activity manager to come back
+        long start = SystemClock.uptimeMillis();
+        IBinder am = null;
+        while (SystemClock.uptimeMillis() - start < MAX_CHECK_AM_TIMEOUT_MS) {
+            am = ServiceManager.checkService(Context.ACTIVITY_SERVICE);
+            if (am != null) {
+                break;
+            } else {
+                Log.d(LOG_TAG, "activity manager not ready yet, continue waiting.");
+                try {
+                    Thread.sleep(CHECK_AM_INTERVAL_MS);
+                } catch (InterruptedException e) {
+                    // break out of current loop upon interruption
+                    break;
+                }
+            }
+        }
+        return am;
+    }
+
+    private boolean checkNativeCrashes() {
+        String[] tombstones = TOMBSTONES_PATH.list();
+
+        // shortcut path for usually empty directory, so we don't waste even
+        // more objects
+        if ((tombstones == null) || (tombstones.length == 0)) {
+            mTombstones = null;
+            return false;
+        }
+
+        // use set logic to look for new files
+        HashSet<String> newStones = new HashSet<String>();
+        for (String x : tombstones) {
+            newStones.add(x);
+        }
+
+        boolean result = (mTombstones == null) || !mTombstones.containsAll(newStones);
+
+        // keep the new list for the next time
+        mTombstones = newStones;
+
+        return result;
+    }
+
+    private class CrashCollector extends IActivityController.Stub {
+
+        @Override
+        public boolean activityStarting(Intent intent, String pkg) throws RemoteException {
+            // check native crashes when we have a chance
+            if (checkNativeCrashes()) {
+                print("NATIVE: new tombstones");
+            }
+            return true;
+        }
+
+        @Override
+        public boolean activityResuming(String pkg) throws RemoteException {
+            // check native crashes when we have a chance
+            if (checkNativeCrashes()) {
+                print("NATIVE: new tombstones");
+            }
+            return true;
+        }
+
+        @Override
+        public boolean appCrashed(String processName, int pid, String shortMsg, String longMsg,
+                long timeMillis, String stackTrace) throws RemoteException {
+            if (processName == null) {
+                print("CRASH: null process name, assuming system");
+            } else {
+                print("CRASH: " + processName);
+            }
+            return false;
+        }
+
+        @Override
+        public int appEarlyNotResponding(String processName, int pid, String annotation)
+                throws RemoteException {
+            // ignore
+            return 0;
+        }
+
+        @Override
+        public int appNotResponding(String processName, int pid, String processStats)
+                throws RemoteException {
+            print("ANR: " + processName);
+            return -1;
+        }
+
+        @Override
+        public int systemNotResponding(String msg) throws RemoteException {
+            print("WATCHDOG: " + msg);
+            return -1;
+        }
+    }
+}
diff --git a/utils/dialogs/Android.mk b/utils/dialogs/Android.mk
new file mode 100644
index 0000000..ae77bf2
--- /dev/null
+++ b/utils/dialogs/Android.mk
@@ -0,0 +1,27 @@
+#
+# 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.
+#
+
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_PACKAGE_NAME := DialogDismissalUtil
+LOCAL_STATIC_JAVA_LIBRARIES := app-helpers ub-uiautomator AuptLib
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_SDK_VERSION := 23
+
+include $(BUILD_PACKAGE)
diff --git a/utils/dialogs/AndroidManifest.xml b/utils/dialogs/AndroidManifest.xml
new file mode 100644
index 0000000..3c1381b
--- /dev/null
+++ b/utils/dialogs/AndroidManifest.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (c) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.test.util.dismissdialogs">
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <uses-sdk android:minSdkVersion="23"
+              android:targetSdkVersion="23" />
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name=".DismissDialogsInstrumentation"
+        android:targetPackage="com.android.test.util.dismissdialogs"
+        android:label="Dismiss Dialog Util">
+    </instrumentation>
+</manifest>
diff --git a/utils/dialogs/src/com/android/dialogutils/DismissDialogsInstrumentation.java b/utils/dialogs/src/com/android/dialogutils/DismissDialogsInstrumentation.java
new file mode 100644
index 0000000..5a42255
--- /dev/null
+++ b/utils/dialogs/src/com/android/dialogutils/DismissDialogsInstrumentation.java
@@ -0,0 +1,272 @@
+/*
+ * 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.test.util.dismissdialogs;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.support.test.aupt.UiWatchers;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.Until;
+import android.util.Log;
+
+import android.platform.test.helpers.IStandardAppHelper;
+import android.platform.test.helpers.ChromeHelperImpl;
+import android.platform.test.helpers.GoogleCameraHelperImpl;
+import android.platform.test.helpers.GoogleKeyboardHelperImpl;
+import android.platform.test.helpers.GmailHelperImpl;
+import android.platform.test.helpers.MapsHelperImpl;
+import android.platform.test.helpers.PhotosHelperImpl;
+import android.platform.test.helpers.PlayMoviesHelperImpl;
+import android.platform.test.helpers.PlayMusicHelperImpl;
+import android.platform.test.helpers.PlayStoreHelperImpl;
+import android.platform.test.helpers.YouTubeHelperImpl;
+import android.support.test.launcherhelper.ILauncherStrategy;
+import android.support.test.launcherhelper.LauncherStrategyFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.NoSuchMethodException;
+import java.lang.InstantiationException;
+import java.lang.IllegalAccessException;
+import java.lang.ReflectiveOperationException;
+import java.lang.reflect.InvocationTargetException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A utility to dismiss all predictable, relevant one-time dialogs
+ */
+public class DismissDialogsInstrumentation extends Instrumentation {
+    private static final String LOG_TAG = DismissDialogsInstrumentation.class.getSimpleName();
+    private static final String IMAGE_SUBFOLDER = "dialog-dismissal";
+
+    private static final long INIT_TIMEOUT = 20000;
+    private static final long MAX_INIT_RETRIES = 5;
+
+    // Comma-separated value indicating for which apps to dismiss dialogs
+    private static final String PARAM_APP = "apps";
+    // Boolean to indicate if this should take screenshots to document dismissal
+    private static final String PARAM_SCREENSHOTS = "screenshots";
+    // Boolean to indicate if this should quit if any failure occurs
+    private static final String PARAM_QUIT_ON_ERROR = "quitOnError";
+
+    // Key for status bundles provided when running the preparer
+    private static final String BUNDLE_DISMISSED_APP_KEY = "dismissedApp";
+    private static final String BUNDLE_APP_ERROR_KEY = "appError";
+
+    private Map<String, Class<? extends IStandardAppHelper>> mKeyHelperMap;
+    private String[] mApps;
+    private boolean mScreenshots;
+    private boolean mQuitOnError;
+    private UiDevice mDevice;
+
+    @Override
+    public void onCreate(Bundle arguments) {
+        super.onCreate(arguments);
+
+        mKeyHelperMap = new HashMap<String, Class<? extends IStandardAppHelper>>();
+        mKeyHelperMap.put("Chrome", ChromeHelperImpl.class);
+        mKeyHelperMap.put("GoogleCamera", GoogleCameraHelperImpl.class);
+        mKeyHelperMap.put("GoogleKeyboard", GoogleKeyboardHelperImpl.class);
+        mKeyHelperMap.put("Gmail", GmailHelperImpl.class);
+        mKeyHelperMap.put("Maps", MapsHelperImpl.class);
+        mKeyHelperMap.put("Photos", PhotosHelperImpl.class);
+        mKeyHelperMap.put("PlayMovies", PlayMoviesHelperImpl.class);
+        mKeyHelperMap.put("PlayMusic", PlayMusicHelperImpl.class);
+        mKeyHelperMap.put("PlayStore", PlayStoreHelperImpl.class);
+        //mKeyHelperMap.put("Settings", SettingsHelperImpl.class);
+        mKeyHelperMap.put("YouTube", YouTubeHelperImpl.class);
+
+        String appsString = arguments.getString(PARAM_APP);
+        if (appsString == null) {
+            throw new IllegalArgumentException("Missing 'apps' parameter.");
+        }
+        mApps = appsString.split(",");
+
+        String screenshotsString = arguments.getString(PARAM_SCREENSHOTS);
+        if (screenshotsString == null) {
+            Log.i(LOG_TAG, "No 'screenshots' parameter. Defaulting to true.");
+            mScreenshots = true;
+        } else {
+            mScreenshots = "true".equals(screenshotsString);
+        }
+
+        String quitString = arguments.getString(PARAM_QUIT_ON_ERROR);
+        if (quitString == null) {
+            Log.i(LOG_TAG, "No 'quitOnError' parameter. Defaulting to quit on error.");
+            mQuitOnError = true;
+        } else {
+            mQuitOnError = "true".equals(quitString);
+        }
+
+        start();
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+
+        mDevice = UiDevice.getInstance(this);
+
+        UiWatchers watcherManager = new UiWatchers();
+        watcherManager.registerAnrAndCrashWatchers(this);
+
+        takeScreenDump("init", "pre-setup");
+
+        try {
+            mDevice.setOrientationNatural();
+        } catch (RemoteException e) {
+            Log.e(LOG_TAG, "Unable to set device orientation.", e);
+        }
+
+        for (int retry = 1; retry <= MAX_INIT_RETRIES; retry++) {
+            ILauncherStrategy launcherStrategy =
+                    LauncherStrategyFactory.getInstance(mDevice).getLauncherStrategy();
+            boolean foundHome = mDevice.wait(Until.hasObject(
+                    launcherStrategy.getWorkspaceSelector()), INIT_TIMEOUT);
+            if (foundHome) {
+                sendStatusUpdate(Activity.RESULT_OK, "launcher");
+                break;
+            } else {
+                takeScreenDump("init", String.format("launcher-selection-failure-%d", retry));
+                if (retry == MAX_INIT_RETRIES && mQuitOnError) {
+                    throw new RuntimeException("Unable to select launcher workspace. Quitting.");
+                } else {
+                    sendStatusUpdate(Activity.RESULT_CANCELED, "launcher");
+                    Log.e(LOG_TAG, "Failed to find home selector; try #" + retry);
+                    // HACK: Try to poke at UI to fix accessibility issue (b/21448825)
+                    try {
+                        mDevice.sleep();
+                        SystemClock.sleep(1000);
+                        mDevice.wakeUp();
+                        mDevice.pressMenu();
+                        UiDevice.getInstance(this).pressHome();
+                    } catch (RemoteException e) {
+                        Log.e(LOG_TAG, "Failed to avoid UI bug b/21448825.", e);
+                    }
+                }
+            }
+        }
+
+        for (String app : mApps) {
+            Log.i(LOG_TAG, String.format("Dismissing dialogs for app, %s.", app));
+            try {
+                if (!dismissDialogs(app)) {
+                    throw new IllegalArgumentException(
+                            String.format("Unrecognized app \"%s\"", mApps));
+                } else {
+                    sendStatusUpdate(Activity.RESULT_OK, app);
+                }
+            } catch (ReflectiveOperationException e) {
+                if (mQuitOnError) {
+                    quitWithError(app, e);
+                } else {
+                    sendStatusUpdate(Activity.RESULT_CANCELED, app);
+                    Log.w(LOG_TAG, "ReflectiveOperationException. Continuing with dismissal.", e);
+                }
+            } catch (RuntimeException e) {
+                if (mQuitOnError) {
+                    quitWithError(app, e);
+                } else {
+                    sendStatusUpdate(Activity.RESULT_CANCELED, app);
+                    Log.w(LOG_TAG, "RuntimeException. Continuing with dismissal.", e);
+                }
+            } catch (AssertionError e) {
+                if (mQuitOnError) {
+                    quitWithError(app, new Exception(e));
+                } else {
+                    sendStatusUpdate(Activity.RESULT_CANCELED, app);
+                    Log.w(LOG_TAG, "AssertionError. Continuing with dismissal.", e);
+                }
+            }
+
+            // Always return to the home page after dismissal
+            UiDevice.getInstance(this).pressHome();
+        }
+
+        watcherManager.removeAnrAndCrashWatchers(this);
+
+        finish(Activity.RESULT_OK, new Bundle());
+    }
+
+    private boolean dismissDialogs(String app) throws NoSuchMethodException, InstantiationException,
+            IllegalAccessException, InvocationTargetException {
+        try {
+            if (mKeyHelperMap.containsKey(app)) {
+                Class<? extends IStandardAppHelper> appHelperClass = mKeyHelperMap.get(app);
+                IStandardAppHelper helper =
+                        appHelperClass.getDeclaredConstructor(Instrumentation.class).newInstance(this);
+                takeScreenDump(app, "-dialog1-pre-open");
+                helper.open();
+                takeScreenDump(app, "-dialog2-pre-dismissal");
+                helper.dismissInitialDialogs();
+                takeScreenDump(app, "-dialog3-post-dismissal");
+                helper.exit();
+                takeScreenDump(app, "-dialog4-post-exit");
+                return true;
+            } else {
+                return false;
+            }
+        } catch (Exception | AssertionError e) {
+            takeScreenDump(app, "-exception");
+            throw e;
+        }
+    }
+
+    private void sendStatusUpdate(int code, String app) {
+        Bundle result = new Bundle();
+        result.putString(BUNDLE_DISMISSED_APP_KEY, app);
+        sendStatus(code, result);
+    }
+
+    private void quitWithError(String app, Exception exception) {
+        Log.e(LOG_TAG, "Quitting with exception.", exception);
+        // Pass Bundle with debugging information to TF
+        Bundle result = new Bundle();
+        result.putString(BUNDLE_DISMISSED_APP_KEY, app);
+        result.putString(BUNDLE_APP_ERROR_KEY, exception.toString());
+        finish(Activity.RESULT_CANCELED, result);
+    }
+
+    private void takeScreenDump(String app, String suffix) {
+        if (!mScreenshots) {
+            return;
+        }
+
+        try {
+            File dir = new File(Environment.getExternalStorageDirectory(), IMAGE_SUBFOLDER);
+            if (!dir.exists() && !dir.mkdirs()) {
+                    throw new RuntimeException(String.format(
+                            "Unable to create or find directory, %s.", dir.getPath()));
+            }
+            File scr = new File(dir, "dd-" + app + suffix + ".png");
+            File uix = new File(dir, "dd-" + app + suffix + ".uix");
+            Log.v(LOG_TAG, String.format("Screen file path: %s", scr.getPath()));
+            Log.v(LOG_TAG, String.format("UI XML file path: %s", uix.getPath()));
+            scr.createNewFile();
+            uix.createNewFile();
+            UiDevice.getInstance(this).takeScreenshot(scr);
+            UiDevice.getInstance(this).dumpWindowHierarchy(uix);
+        } catch (IOException e) {
+            Log.e(LOG_TAG, "Failed screen dump.", e);
+        }
+    }
+}