Merge "Initial PERF tests for autofill."
diff --git a/apct-tests/perftests/core/Android.mk b/apct-tests/perftests/core/Android.mk
index 6156a0c..b5a76cf 100644
--- a/apct-tests/perftests/core/Android.mk
+++ b/apct-tests/perftests/core/Android.mk
@@ -10,6 +10,7 @@
 
 LOCAL_STATIC_JAVA_LIBRARIES := \
     android-support-test \
+    androidx.annotation_annotation \
     apct-perftests-utils \
     guava
 
diff --git a/apct-tests/perftests/core/AndroidManifest.xml b/apct-tests/perftests/core/AndroidManifest.xml
index 132a2f9..13c24d9 100644
--- a/apct-tests/perftests/core/AndroidManifest.xml
+++ b/apct-tests/perftests/core/AndroidManifest.xml
@@ -16,6 +16,16 @@
           </intent-filter>
         </activity>
         <service android:name="android.os.SomeService" android:exported="false" android:process=":some_service" />
+
+        <service
+            android:name="android.view.autofill.MyAutofillService"
+            android:label="PERF AutofillService"
+            android:permission="android.permission.BIND_AUTOFILL_SERVICE" >
+            <intent-filter>
+                <action android:name="android.service.autofill.AutofillService" />
+            </intent-filter>
+        </service>
+
     </application>
 
     <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
diff --git a/apct-tests/perftests/core/res/layout/autofill_dataset_picker_text_only.xml b/apct-tests/perftests/core/res/layout/autofill_dataset_picker_text_only.xml
new file mode 100644
index 0000000..5e6b277
--- /dev/null
+++ b/apct-tests/perftests/core/res/layout/autofill_dataset_picker_text_only.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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="horizontal">
+
+    <TextView
+        android:id="@+id/text"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"/>
+</LinearLayout>
\ No newline at end of file
diff --git a/apct-tests/perftests/core/res/layout/test_autofill_login.xml b/apct-tests/perftests/core/res/layout/test_autofill_login.xml
new file mode 100644
index 0000000..b35bdf1
--- /dev/null
+++ b/apct-tests/perftests/core/res/layout/test_autofill_login.xml
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:focusable="true"
+    android:focusableInTouchMode="true"
+    android:orientation="vertical" >
+
+    <LinearLayout
+        android:id="@+id/username_container"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal" >
+
+        <TextView
+            android:id="@+id/username_label"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="Username" />
+
+        <EditText
+            android:id="@+id/username"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:imeOptions="flagNoFullscreen" />
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal" >
+
+        <TextView
+            android:id="@+id/password_label"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="Password" />
+
+        <EditText
+            android:id="@+id/password"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:inputType="textPassword"
+            android:imeOptions="flagNoFullscreen" />
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal" >
+
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal" >
+
+        <Button
+            android:id="@+id/login"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="Login" />
+
+        <Button
+            android:id="@+id/cancel"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="Cancel" />
+    </LinearLayout>
+
+    <TextView
+        android:id="@+id/output"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content" />
+
+</LinearLayout>
diff --git a/apct-tests/perftests/core/src/android/view/autofill/AutofillPerfTest.java b/apct-tests/perftests/core/src/android/view/autofill/AutofillPerfTest.java
new file mode 100644
index 0000000..bc92aab
--- /dev/null
+++ b/apct-tests/perftests/core/src/android/view/autofill/AutofillPerfTest.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.view.autofill;
+
+import android.app.Activity;
+import android.os.Looper;
+import android.os.Bundle;
+import android.perftests.utils.PerfStatusReporter;
+import android.perftests.utils.SettingsHelper;
+import android.perftests.utils.ShellHelper;
+import android.util.Log;
+import android.view.View;
+import android.widget.EditText;
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.StubActivity;
+import android.provider.Settings;
+import android.support.test.filters.LargeTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.InstrumentationRegistry;
+
+import com.android.perftests.core.R;
+
+import java.util.Locale;
+import java.util.Collection;
+import java.util.Arrays;
+
+import org.junit.Test;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertTrue;
+
+@RunWith(Parameterized.class)
+public class AutofillPerfTest {
+    @Parameters(name = "{0}")
+    @SuppressWarnings("rawtypes")
+    public static Collection layouts() {
+        return Arrays.asList(new Object[][] {
+                { "Simple login", R.layout.test_autofill_login}
+        });
+    }
+
+    private final int mLayoutId;
+    private EditText mUsername;
+    private EditText mPassword;
+
+    public AutofillPerfTest(String key, int layoutId) {
+        mLayoutId = layoutId;
+    }
+
+    @Rule
+    public ActivityTestRule<StubActivity> mActivityRule =
+            new ActivityTestRule<StubActivity>(StubActivity.class);
+
+    @Rule
+    public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+    /**
+     * Prepares the activity so that by the time the test is run it has reference to its fields.
+     */
+    @Before
+    public void prepareActivity() throws Throwable {
+        mActivityRule.runOnUiThread(() -> {
+            assertTrue("We should be running on the main thread",
+                    Looper.getMainLooper().getThread() == Thread.currentThread());
+            assertTrue("We should be running on the main thread",
+                    Looper.myLooper() == Looper.getMainLooper());
+            Activity activity = mActivityRule.getActivity();
+            activity.setContentView(mLayoutId);
+            View root = activity.getWindow().getDecorView();
+            mUsername = root.findViewById(R.id.username);
+            mPassword = root.findViewById(R.id.password);
+        });
+    }
+
+    @Before
+    public void resetStaticState() {
+        MyAutofillService.resetStaticState();
+    }
+
+    @After
+    public void cleanup() {
+        resetService();
+    }
+
+    /**
+     * This is the baseline test for focusing the 2 views when autofill is disabled.
+     */
+    @Test
+    public void testFocus_noService() throws Throwable {
+        resetService();
+
+        focusTest();
+    }
+
+    /**
+     * This time the service is called, but it returns a {@code null} response so the UI behaves
+     * as if autofill was disabled.
+     */
+    @Test
+    public void testFocus_serviceDoesNotAutofill() throws Throwable {
+        MyAutofillService.newCannedResponse().reply();
+        setService();
+
+        // Must first focus in a field to trigger autofill and wait for service response
+        // outside the loop
+        mActivityRule.runOnUiThread(() -> mUsername.requestFocus());
+        MyAutofillService.getLastFillRequest();
+
+        // Test properly speaking
+        focusTest();
+
+        // Sanity check
+        MyAutofillService.assertNoAsyncErrors();
+    }
+
+    /**
+     * Now the service returns autofill data, for both username and password.
+     */
+    @Test
+    public void testFocus_autofillBothFields() throws Throwable {
+        MyAutofillService.newCannedResponse()
+                .setUsername(mUsername.getAutofillId(), "user")
+                .setPassword(mPassword.getAutofillId(), "pass")
+                .reply();
+        setService();
+
+        // Must first focus in a field to trigger autofill and wait for service response
+        // outside the loop
+        mActivityRule.runOnUiThread(() -> mUsername.requestFocus());
+        MyAutofillService.getLastFillRequest();
+
+        // Test properly speaking
+        focusTest();
+
+        // Sanity check
+        MyAutofillService.assertNoAsyncErrors();
+    }
+
+    /**
+     * Now the service returns autofill data, but just for username.
+     */
+    @Test
+    public void testFocus_autofillUsernameOnly() throws Throwable {
+        // Must set ignored ids so focus on password does not trigger new requests
+        MyAutofillService.newCannedResponse()
+                .setUsername(mUsername.getAutofillId(), "user")
+                .setIgnored(mPassword.getAutofillId())
+                .reply();
+        setService();
+
+        // Must first focus in a field to trigger autofill and wait for service response
+        // outside the loop
+        mActivityRule.runOnUiThread(() -> mUsername.requestFocus());
+        MyAutofillService.getLastFillRequest();
+
+        focusTest();
+
+        // Sanity check
+        MyAutofillService.assertNoAsyncErrors();
+    }
+
+    private void focusTest() throws Throwable {
+        mActivityRule.runOnUiThread(() -> {
+            BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+            while (state.keepRunning()) {
+                mUsername.requestFocus();
+                mPassword.requestFocus();
+            }
+        });
+    }
+
+    // TODO: add tests for changing value of the fields
+
+    /**
+     * Uses the {@code settings} binary to set the autofill service.
+     */
+    private void setService() {
+        SettingsHelper.syncSet(InstrumentationRegistry.getTargetContext(),
+                SettingsHelper.NAMESPACE_SECURE,
+                Settings.Secure.AUTOFILL_SERVICE,
+                MyAutofillService.COMPONENT_NAME);
+    }
+
+    /**
+     * Uses the {@code settings} binary to reset the autofill service.
+     */
+    private void resetService() {
+        SettingsHelper.syncDelete(InstrumentationRegistry.getTargetContext(),
+                SettingsHelper.NAMESPACE_SECURE,
+                Settings.Secure.AUTOFILL_SERVICE);
+    }
+}
diff --git a/apct-tests/perftests/core/src/android/view/autofill/MyAutofillService.java b/apct-tests/perftests/core/src/android/view/autofill/MyAutofillService.java
new file mode 100644
index 0000000..16eeb3b
--- /dev/null
+++ b/apct-tests/perftests/core/src/android/view/autofill/MyAutofillService.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.view.autofill;
+
+import android.os.CancellationSignal;
+import android.service.autofill.AutofillService;
+import android.service.autofill.Dataset;
+import android.service.autofill.FillCallback;
+import android.service.autofill.FillRequest;
+import android.service.autofill.FillResponse;
+import android.service.autofill.SaveCallback;
+import android.service.autofill.SaveRequest;
+import android.util.Log;
+import android.util.Pair;
+import android.widget.RemoteViews;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.perftests.core.R;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * An {@link AutofillService} implementation whose replies can be programmed by the test case.
+ */
+public class MyAutofillService extends AutofillService {
+
+    private static final String TAG = "MyAutofillService";
+    private static final int TIMEOUT_MS = 5000;
+
+    private static final String PACKAGE_NAME = "com.android.perftests.core";
+    static final String COMPONENT_NAME = PACKAGE_NAME + "/android.view.autofill.MyAutofillService";
+
+    private static final BlockingQueue<FillRequest> sFillRequests = new LinkedBlockingQueue<>();
+    private static final BlockingQueue<CannedResponse> sCannedResponses =
+            new LinkedBlockingQueue<>();
+    private static final List<String> sAsyncErrors = new ArrayList<>();
+
+    /**
+     * Resets the static state associated with the service.
+     */
+    static void resetStaticState() {
+        sFillRequests.clear();
+        sCannedResponses.clear();
+        sAsyncErrors.clear();
+    }
+
+    /**
+     * Throws an exception if an error happened asynchronously while handing
+     * {@link #onFillRequest(FillRequest, CancellationSignal, FillCallback)}.
+     */
+    static void assertNoAsyncErrors() {
+       if (!sAsyncErrors.isEmpty()) {
+           throw new IllegalStateException("got errors: " + sAsyncErrors);
+       }
+    }
+
+    /**
+     * Gets the the last {@link FillRequest} passed to
+     * {@link #onFillRequest(FillRequest, CancellationSignal, FillCallback)} or throws an
+     * exception if that method was not called.
+     */
+    @NonNull
+    static FillRequest getLastFillRequest() {
+        FillRequest request = null;
+        try {
+            request = sFillRequests.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new IllegalStateException("onFillRequest() interrupted");
+        }
+        if (request == null) {
+            throw new IllegalStateException("onFillRequest() not called in " + TIMEOUT_MS + "ms");
+        }
+        return request;
+    }
+
+    @Override
+    public void onFillRequest(FillRequest request, CancellationSignal cancellationSignal,
+            FillCallback callback) {
+        CannedResponse response = null;
+        try {
+            response = sCannedResponses.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        } catch (InterruptedException e) {
+            addAsyncError("onFillRequest() interrupted");
+            Thread.currentThread().interrupt();
+            return;
+        }
+        if (response == null) {
+            addAsyncError("onFillRequest() called without setting a response");
+            return;
+        }
+        try {
+            Dataset.Builder dataset = new Dataset.Builder(newDatasetPresentation("dataset"));
+            boolean hasData = false;
+            if (response.mUsername != null) {
+                hasData = true;
+                dataset.setValue(response.mUsername.first,
+                        AutofillValue.forText(response.mUsername.second));
+            }
+            if (response.mPassword != null) {
+                hasData = true;
+                dataset.setValue(response.mPassword.first,
+                        AutofillValue.forText(response.mPassword.second));
+            }
+            if (hasData) {
+                FillResponse.Builder fillResponse = new FillResponse.Builder();
+                if (response.mIgnoredIds != null) {
+                    fillResponse.setIgnoredIds(response.mIgnoredIds);
+                }
+
+                callback.onSuccess(fillResponse.addDataset(dataset.build()).build());
+            } else {
+                callback.onSuccess(null);
+            }
+        } catch (Exception e) {
+            addAsyncError(e, callback);
+        }
+        sFillRequests.offer(request);
+    }
+
+    @Override
+    public void onSaveRequest(SaveRequest request, SaveCallback callback) {
+        // No current test should have triggered it...
+        callback.onFailure("should not have called onSave");
+    }
+
+    static final class CannedResponse {
+        private final Pair<AutofillId, String> mUsername;
+        private final Pair<AutofillId, String> mPassword;
+        private final AutofillId[] mIgnoredIds;
+
+        private CannedResponse(@NonNull Builder builder) {
+            mUsername = builder.mUsername;
+            mPassword = builder.mPassword;
+            mIgnoredIds = builder.mIgnoredIds;
+        }
+
+        static class Builder {
+            private Pair<AutofillId, String> mUsername;
+            private Pair<AutofillId, String> mPassword;
+            private AutofillId[] mIgnoredIds;
+
+            @NonNull
+            Builder setUsername(@NonNull AutofillId id, @NonNull String value) {
+                mUsername = new Pair<>(id, value);
+                return this;
+            }
+
+            @NonNull
+            Builder setPassword(@NonNull AutofillId id, @NonNull String value) {
+                mPassword = new Pair<>(id, value);
+                return this;
+            }
+
+            @NonNull
+            Builder setIgnored(AutofillId... ids) {
+                mIgnoredIds = ids;
+                return this;
+            }
+
+            void reply() {
+                sCannedResponses.add(new CannedResponse(this));
+            }
+        }
+    }
+
+    /**
+     * Sets the expected canned {@link FillResponse} for the next
+     * {@link AutofillService#onFillRequest(FillRequest, CancellationSignal, FillCallback)}.
+     */
+    static CannedResponse.Builder newCannedResponse() {
+        return new CannedResponse.Builder();
+    }
+
+    private void addAsyncError(@NonNull String error) {
+        sAsyncErrors.add(error);
+        Log.e(TAG, error);
+    }
+
+    private void addAsyncError(@NonNull Exception e, @NonNull FillCallback callback) {
+        String msg = e.toString();
+        sAsyncErrors.add(msg);
+        Log.e(TAG, "async error", e);
+        callback.onFailure(msg);
+    }
+
+    @NonNull
+    private static RemoteViews newDatasetPresentation(@NonNull CharSequence text) {
+        RemoteViews presentation =
+                new RemoteViews(PACKAGE_NAME, R.layout.autofill_dataset_picker_text_only);
+        presentation.setTextViewText(R.id.text, text);
+        return presentation;
+    }
+}
diff --git a/apct-tests/perftests/multiuser/Android.mk b/apct-tests/perftests/multiuser/Android.mk
index 9bc7d05..5ff4ebc 100644
--- a/apct-tests/perftests/multiuser/Android.mk
+++ b/apct-tests/perftests/multiuser/Android.mk
@@ -21,7 +21,7 @@
 
 LOCAL_STATIC_JAVA_LIBRARIES := \
     android-support-test \
-    ub-uiautomator
+    apct-perftests-utils
 
 LOCAL_PACKAGE_NAME := MultiUserPerfTests
 LOCAL_PRIVATE_PLATFORM_APIS := true
diff --git a/apct-tests/perftests/multiuser/src/android/multiuser/BenchmarkRunner.java b/apct-tests/perftests/multiuser/src/android/multiuser/BenchmarkRunner.java
index 629e6f4..7b65bfa 100644
--- a/apct-tests/perftests/multiuser/src/android/multiuser/BenchmarkRunner.java
+++ b/apct-tests/perftests/multiuser/src/android/multiuser/BenchmarkRunner.java
@@ -17,10 +17,8 @@
 
 import android.os.Bundle;
 import android.os.SystemClock;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.uiautomator.UiDevice;
+import android.perftests.utils.ShellHelper;
 
-import java.io.IOException;
 import java.util.ArrayList;
 
 // Based on //platform/frameworks/base/apct-tests/perftests/utils/BenchmarkState.java
@@ -74,12 +72,7 @@
 
     private void prepareForNextRun() {
         SystemClock.sleep(COOL_OFF_PERIOD_MS);
-        try {
-            UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
-                    .executeShellCommand("am wait-for-broadcast-idle");
-        } catch (IOException e) {
-            throw new IllegalStateException("Cannot execute shell command", e);
-        }
+        ShellHelper.runShellCommand("am wait-for-broadcast-idle");
         mStartTimeNs = System.nanoTime();
         mPausedDurationNs = 0;
     }
diff --git a/apct-tests/perftests/utils/Android.mk b/apct-tests/perftests/utils/Android.mk
index 55c13b0..604f0ad 100644
--- a/apct-tests/perftests/utils/Android.mk
+++ b/apct-tests/perftests/utils/Android.mk
@@ -1,7 +1,9 @@
 LOCAL_PATH := $(call my-dir)
 include $(CLEAR_VARS)
 
-LOCAL_STATIC_JAVA_LIBRARIES := android-support-test
+LOCAL_STATIC_JAVA_LIBRARIES := \
+    android-support-test \
+    androidx.annotation_annotation
 
 # Build all java files in the java subdirectory
 LOCAL_SRC_FILES := $(call all-subdir-java-files)
diff --git a/apct-tests/perftests/utils/src/android/perftests/utils/OneTimeSettingsListener.java b/apct-tests/perftests/utils/src/android/perftests/utils/OneTimeSettingsListener.java
new file mode 100644
index 0000000..37af4c7
--- /dev/null
+++ b/apct-tests/perftests/utils/src/android/perftests/utils/OneTimeSettingsListener.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.perftests.utils;
+
+import static android.perftests.utils.SettingsHelper.NAMESPACE_SECURE;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.Settings;
+
+import androidx.annotation.NonNull;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Helper used to block tests until a secure settings value has been updated.
+ */
+public final class OneTimeSettingsListener extends ContentObserver {
+    private final CountDownLatch mLatch = new CountDownLatch(1);
+    private final ContentResolver mResolver;
+    private final String mKey;
+    private final int mTimeoutMs;
+
+    public OneTimeSettingsListener(@NonNull Context context, @NonNull String namespace,
+            @NonNull String key, int timeoutMs) {
+        super(new Handler(Looper.getMainLooper()));
+        mKey = key;
+        mResolver = context.getContentResolver();
+        mTimeoutMs = timeoutMs;
+        final Uri uri;
+        switch (namespace) {
+            case NAMESPACE_SECURE:
+                uri = Settings.Secure.getUriFor(key);
+                break;
+            default:
+                throw new IllegalArgumentException("invalid namespace: " + namespace);
+        }
+        mResolver.registerContentObserver(uri, false, this);
+    }
+
+    @Override
+    public void onChange(boolean selfChange, Uri uri) {
+        mResolver.unregisterContentObserver(this);
+        mLatch.countDown();
+    }
+
+    /**
+     * Blocks for a few seconds until it's called, or throws an {@link IllegalStateException} if
+     * it isn't.
+     */
+    public void assertCalled() {
+        try {
+            final boolean updated = mLatch.await(mTimeoutMs, TimeUnit.MILLISECONDS);
+            if (!updated) {
+                throw new IllegalStateException(
+                        "Settings " + mKey + " not called in " + mTimeoutMs + "ms");
+            }
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new IllegalStateException("Interrupted", e);
+        }
+    }
+}
diff --git a/apct-tests/perftests/utils/src/android/perftests/utils/SettingsHelper.java b/apct-tests/perftests/utils/src/android/perftests/utils/SettingsHelper.java
new file mode 100644
index 0000000..d7d1d6b
--- /dev/null
+++ b/apct-tests/perftests/utils/src/android/perftests/utils/SettingsHelper.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.perftests.utils;
+
+import android.content.Context;
+import android.provider.Settings;
+import android.text.TextUtils;
+
+import java.util.Objects;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * Provides utilities to interact with the device's {@link Settings}.
+ */
+public final class SettingsHelper {
+
+    public static final String NAMESPACE_SECURE = "secure";
+
+    private static int DEFAULT_TIMEOUT_MS = 5000;
+
+    /**
+     * Uses a Shell command to "asynchronously" set the given preference, returning right away.
+     */
+    public static void set(@NonNull String namespace, @NonNull String key, @Nullable String value) {
+        if (value == null) {
+            delete(namespace, key);
+            return;
+        }
+        ShellHelper.runShellCommand("settings put %s %s %s default", namespace, key, value);
+    }
+
+    /**
+     * Uses a Shell command to "synchronously" set the given preference by registering a listener
+     * and wait until it's set.
+     */
+    public static void syncSet(@NonNull Context context, @NonNull String namespace,
+            @NonNull String key, @Nullable String value) {
+        if (value == null) {
+            syncDelete(context, namespace, key);
+            return;
+        }
+
+        String currentValue = get(namespace, key);
+        if (value.equals(currentValue)) {
+            // Already set, ignore
+            return;
+        }
+
+        OneTimeSettingsListener observer = new OneTimeSettingsListener(context, namespace, key,
+                DEFAULT_TIMEOUT_MS);
+        set(namespace, key, value);
+        observer.assertCalled();
+        assertNewValue(namespace, key, value);
+    }
+
+    /**
+     * Uses a Shell command to "asynchronously" delete the given preference, returning right away.
+     */
+    public static void delete(@NonNull String namespace, @NonNull String key) {
+        ShellHelper.runShellCommand("settings delete %s %s", namespace, key);
+    }
+
+    /**
+     * Uses a Shell command to "synchronously" delete the given preference by registering a listener
+     * and wait until it's called.
+     */
+    public static void syncDelete(@NonNull Context context, @NonNull String namespace,
+            @NonNull String key) {
+        String currentValue = get(namespace, key);
+        if (currentValue == null || currentValue.equals("null")) {
+            // Already set, ignore
+            return;
+        }
+
+        OneTimeSettingsListener observer = new OneTimeSettingsListener(context, namespace, key,
+                DEFAULT_TIMEOUT_MS);
+        delete(namespace, key);
+        observer.assertCalled();
+        assertNewValue(namespace, key, "null");
+    }
+
+    /**
+     * Gets the value of a given preference using Shell command.
+     */
+    @NonNull
+    public static String get(@NonNull String namespace, @NonNull String key) {
+        return ShellHelper.runShellCommand("settings get %s %s", namespace, key);
+    }
+
+    private static void assertNewValue(@NonNull String namespace, @NonNull String key,
+            @Nullable String expectedValue) {
+        String actualValue = get(namespace, key);
+        if (!Objects.equals(actualValue, expectedValue)) {
+            throw new AssertionError("invalid value for " + namespace + ":" + key + ": expected '"
+                    + actualValue + "' , got '" + expectedValue + "'");
+        }
+    }
+
+    private SettingsHelper() {
+        throw new UnsupportedOperationException("contain static methods only");
+    }
+}
diff --git a/apct-tests/perftests/utils/src/android/perftests/utils/ShellHelper.java b/apct-tests/perftests/utils/src/android/perftests/utils/ShellHelper.java
new file mode 100644
index 0000000..cae87fb
--- /dev/null
+++ b/apct-tests/perftests/utils/src/android/perftests/utils/ShellHelper.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.perftests.utils;
+
+import android.app.UiAutomation;
+import android.os.ParcelFileDescriptor;
+import android.support.test.InstrumentationRegistry;
+import android.text.TextUtils;
+import android.util.AndroidRuntimeException;
+import android.util.Log;
+
+import java.io.FileInputStream;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Provides Shell-based utilities such as running a command.
+ */
+public final class ShellHelper {
+
+    /**
+     * Runs a Shell command, returning a trimmed response.
+     */
+    @NonNull
+    public static String runShellCommand(@NonNull String template, Object...args) {
+        String command = String.format(template, args);
+        UiAutomation automan = InstrumentationRegistry.getInstrumentation()
+                .getUiAutomation();
+        ParcelFileDescriptor pfd = automan.executeShellCommand(command);
+        byte[] buf = new byte[512];
+        int bytesRead;
+        try(FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(pfd)) {
+            StringBuilder stdout = new StringBuilder();
+            while ((bytesRead = fis.read(buf)) != -1) {
+                stdout.append(new String(buf, 0, bytesRead));
+            }
+            String result = stdout.toString();
+            return TextUtils.isEmpty(result) ? "" : result.trim();
+        } catch (Exception e) {
+            throw new AndroidRuntimeException("Command '" + command + "' failed: ", e);
+        } finally {
+            // Must disconnect UI automation after every call, otherwise its accessibility service
+            // skews the performance tests.
+            automan.destroy();
+        }
+    }
+
+    private ShellHelper() {
+        throw new UnsupportedOperationException("contain static methods only");
+    }
+}