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");
+ }
+}