Add content capture performance test cases

Bug: 153770130
Test: atest ContentCapturePerfTests
Change-Id: I6b0226ece380733b364cf4ebd0ee80daf70c56f2
diff --git a/apct-tests/perftests/contentcapture/Android.bp b/apct-tests/perftests/contentcapture/Android.bp
new file mode 100644
index 0000000..66d7348
--- /dev/null
+++ b/apct-tests/perftests/contentcapture/Android.bp
@@ -0,0 +1,28 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+android_test {
+    name: "ContentCapturePerfTests",
+    srcs: ["src/**/*.java"],
+    resource_dirs: ["res"],
+    static_libs: [
+        "androidx.test.rules",
+        "androidx.annotation_annotation",
+        "apct-perftests-utils",
+        "collector-device-lib-platform",
+        "compatibility-device-util-axt",
+    ],
+    platform_apis: true,
+    test_suites: ["device-tests"],
+}
diff --git a/apct-tests/perftests/contentcapture/AndroidManifest.xml b/apct-tests/perftests/contentcapture/AndroidManifest.xml
new file mode 100644
index 0000000..ee5577f
--- /dev/null
+++ b/apct-tests/perftests/contentcapture/AndroidManifest.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT 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.perftests.contentcapture">
+
+    <uses-sdk android:targetSdkVersion="28" />
+
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.REAL_GET_TASKS" />
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+        <activity android:name="android.view.contentcapture.CustomTestActivity"
+                android:exported="true">
+        </activity>
+
+        <service
+            android:name="android.view.contentcapture.MyContentCaptureService"
+            android:label="PERF ContentCaptureService"
+            android:permission="android.permission.BIND_CONTENT_CAPTURE_SERVICE"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.service.contentcapture.ContentCaptureService" />
+            </intent-filter>
+        </service>
+
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.perftests.contentcapture" />
+</manifest>
diff --git a/apct-tests/perftests/contentcapture/AndroidTest.xml b/apct-tests/perftests/contentcapture/AndroidTest.xml
new file mode 100644
index 0000000..d2386bb
--- /dev/null
+++ b/apct-tests/perftests/contentcapture/AndroidTest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Runs ContentCapturePerfTests metric instrumentation.">
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-suite-tag" value="apct-metric-instrumentation" />
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="ContentCapturePerfTests.apk" />
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.perftests.contentcapture" />
+    </test>
+</configuration>
diff --git a/apct-tests/perftests/contentcapture/res/layout/test_container_activity.xml b/apct-tests/perftests/contentcapture/res/layout/test_container_activity.xml
new file mode 100644
index 0000000..ca1a11a
--- /dev/null
+++ b/apct-tests/perftests/contentcapture/res/layout/test_container_activity.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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:id="@+id/root_view"
+              android:layout_width="match_parent"
+              android:layout_height="match_parent"
+              android:focusable="true"
+              android:focusableInTouchMode="true"
+              android:orientation="vertical" >
+</LinearLayout>
diff --git a/apct-tests/perftests/contentcapture/res/layout/test_login_activity.xml b/apct-tests/perftests/contentcapture/res/layout/test_login_activity.xml
new file mode 100644
index 0000000..9bab32c
--- /dev/null
+++ b/apct-tests/perftests/contentcapture/res/layout/test_login_activity.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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:id="@+id/root_view"
+              android:layout_width="match_parent"
+              android:layout_height="match_parent"
+              android:focusable="true"
+              android:focusableInTouchMode="true"
+              android:orientation="vertical" >
+
+    <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" />
+
+    <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" />
+
+</LinearLayout>
diff --git a/apct-tests/perftests/contentcapture/src/android/view/contentcapture/AbstractContentCapturePerfTestCase.java b/apct-tests/perftests/contentcapture/src/android/view/contentcapture/AbstractContentCapturePerfTestCase.java
new file mode 100644
index 0000000..f02c96d
--- /dev/null
+++ b/apct-tests/perftests/contentcapture/src/android/view/contentcapture/AbstractContentCapturePerfTestCase.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.contentcapture;
+
+import static android.view.contentcapture.CustomTestActivity.INTENT_EXTRA_CUSTOM_VIEWS;
+import static android.view.contentcapture.CustomTestActivity.INTENT_EXTRA_LAYOUT_ID;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.compatibility.common.util.ShellUtils.runShellCommand;
+
+import android.app.Application;
+import android.content.ContentCaptureOptions;
+import android.content.Context;
+import android.content.Intent;
+import android.os.BatteryManager;
+import android.os.UserHandle;
+import android.perftests.utils.PerfStatusReporter;
+import android.provider.Settings;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.test.filters.LargeTest;
+import androidx.test.rule.ActivityTestRule;
+
+import com.android.compatibility.common.util.ActivitiesWatcher;
+import com.android.compatibility.common.util.ActivitiesWatcher.ActivityWatcher;
+import com.android.perftests.contentcapture.R;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.rules.TestRule;
+import org.junit.runners.model.Statement;
+
+/**
+ * Base class for all content capture tests.
+ */
+@LargeTest
+public abstract class AbstractContentCapturePerfTestCase {
+
+    private static final String TAG = AbstractContentCapturePerfTestCase.class.getSimpleName();
+    private static final long GENERIC_TIMEOUT_MS = 10_000;
+
+    private static int sOriginalStayOnWhilePluggedIn;
+    private static Context sContext = getInstrumentation().getTargetContext();
+
+    protected ActivitiesWatcher mActivitiesWatcher;
+
+    private MyContentCaptureService.ServiceWatcher mServiceWatcher;
+
+    @Rule
+    public ActivityTestRule<CustomTestActivity> mActivityRule =
+            new ActivityTestRule<>(CustomTestActivity.class, false, false);
+
+    @Rule
+    public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+    @Rule
+    public TestRule mServiceDisablerRule = (base, description) -> {
+        return new Statement() {
+            @Override
+            public void evaluate() throws Throwable {
+                try {
+                    base.evaluate();
+                } finally {
+                    Log.v(TAG, "@mServiceDisablerRule: safelyDisableService()");
+                    safelyDisableService();
+                }
+            }
+        };
+    };
+
+    private void safelyDisableService() {
+        try {
+            resetService();
+            MyContentCaptureService.resetStaticState();
+
+            if (mServiceWatcher != null) {
+                mServiceWatcher.waitOnDestroy();
+            }
+        } catch (Throwable t) {
+            Log.e(TAG, "error disabling service", t);
+        }
+    }
+
+    /**
+     * Sets the content capture service.
+     */
+    private static void setService(@NonNull String service) {
+        final int userId = getCurrentUserId();
+        Log.d(TAG, "Setting service for user " + userId + " to " + service);
+        // TODO(b/123540602): use @TestingAPI to get max duration constant
+        runShellCommand("cmd content_capture set temporary-service %d %s 119000", userId, service);
+    }
+
+    /**
+     * Resets the content capture service.
+     */
+    private static void resetService() {
+        final int userId = getCurrentUserId();
+        Log.d(TAG, "Resetting back user " + userId + " to default service");
+        runShellCommand("cmd content_capture set temporary-service %d", userId);
+    }
+
+    private static int getCurrentUserId() {
+        return UserHandle.myUserId();
+    }
+
+    @BeforeClass
+    public static void setStayAwake() {
+        Log.v(TAG, "@BeforeClass: setStayAwake()");
+        // Some test cases will restart the activity, and stay awake is necessary to ensure that
+        // the test will not screen off during the test.
+        // Keeping the activity screen on is not enough, screen off may occur between the activity
+        // finished and the next start
+        final int stayOnWhilePluggedIn = Settings.Global.getInt(sContext.getContentResolver(),
+                Settings.Global.STAY_ON_WHILE_PLUGGED_IN, 0);
+        sOriginalStayOnWhilePluggedIn = -1;
+        if (stayOnWhilePluggedIn != BatteryManager.BATTERY_PLUGGED_ANY) {
+            sOriginalStayOnWhilePluggedIn = stayOnWhilePluggedIn;
+            // Keep the device awake during testing.
+            setStayOnWhilePluggedIn(BatteryManager.BATTERY_PLUGGED_ANY);
+        }
+    }
+
+    @AfterClass
+    public static void resetStayAwake() {
+        Log.v(TAG, "@AfterClass: resetStayAwake()");
+        if (sOriginalStayOnWhilePluggedIn != -1) {
+            setStayOnWhilePluggedIn(sOriginalStayOnWhilePluggedIn);
+        }
+    }
+
+    private static void setStayOnWhilePluggedIn(int value) {
+        runShellCommand(String.format("settings put global %s %d",
+                Settings.Global.STAY_ON_WHILE_PLUGGED_IN, value));
+    }
+
+    @BeforeClass
+    public static void setAllowSelf() {
+        final ContentCaptureOptions options = new ContentCaptureOptions(null);
+        Log.v(TAG, "@BeforeClass: setAllowSelf(): options=" + options);
+        sContext.getApplicationContext().setContentCaptureOptions(options);
+    }
+
+    @AfterClass
+    public static void unsetAllowSelf() {
+        Log.v(TAG, "@AfterClass: unsetAllowSelf()");
+        clearOptions();
+    }
+
+    protected static void clearOptions() {
+        sContext.getApplicationContext().setContentCaptureOptions(null);
+    }
+
+    @BeforeClass
+    public static void disableDefaultService() {
+        Log.v(TAG, "@BeforeClass: disableDefaultService()");
+        setDefaultServiceEnabled(false);
+    }
+
+    @AfterClass
+    public static void enableDefaultService() {
+        Log.v(TAG, "@AfterClass: enableDefaultService()");
+        setDefaultServiceEnabled(true);
+    }
+
+    /**
+     * Enables / disables the default service.
+     */
+    private static void setDefaultServiceEnabled(boolean enabled) {
+        final int userId = getCurrentUserId();
+        Log.d(TAG, "setDefaultServiceEnabled(user=" + userId + ", enabled= " + enabled + ")");
+        runShellCommand("cmd content_capture set default-service-enabled %d %s", userId,
+                Boolean.toString(enabled));
+    }
+
+    @Before
+    public void prepareDevice() throws Exception {
+        Log.v(TAG, "@Before: prepareDevice()");
+
+        // Unlock screen.
+        runShellCommand("input keyevent KEYCODE_WAKEUP");
+
+        // Dismiss keyguard, in case it's set as "Swipe to unlock".
+        runShellCommand("wm dismiss-keyguard");
+
+        // Collapse notifications.
+        runShellCommand("cmd statusbar collapse");
+    }
+
+    @Before
+    public void registerLifecycleCallback() {
+        Log.v(TAG, "@Before: Registering lifecycle callback");
+        final Application app = (Application) sContext.getApplicationContext();
+        mActivitiesWatcher = new ActivitiesWatcher(GENERIC_TIMEOUT_MS);
+        app.registerActivityLifecycleCallbacks(mActivitiesWatcher);
+    }
+
+    @After
+    public void unregisterLifecycleCallback() {
+        Log.d(TAG, "@After: Unregistering lifecycle callback: " + mActivitiesWatcher);
+        if (mActivitiesWatcher != null) {
+            final Application app = (Application) sContext.getApplicationContext();
+            app.unregisterActivityLifecycleCallbacks(mActivitiesWatcher);
+        }
+    }
+
+    /**
+     * Sets {@link MyContentCaptureService} as the service for the current user and waits until
+     * its created, then add the perf test package into allow list.
+     */
+    public MyContentCaptureService enableService() throws InterruptedException {
+        if (mServiceWatcher != null) {
+            throw new IllegalStateException("There Can Be Only One!");
+        }
+
+        mServiceWatcher = MyContentCaptureService.setServiceWatcher();
+        setService(MyContentCaptureService.SERVICE_NAME);
+        mServiceWatcher.setAllowSelf();
+        return mServiceWatcher.waitOnCreate();
+    }
+
+    @NonNull
+    protected ActivityWatcher startWatcher() {
+        return mActivitiesWatcher.watch(CustomTestActivity.class);
+    }
+
+    /**
+     * Launch test activity with default login layout
+     */
+    protected CustomTestActivity launchActivity() {
+        return launchActivity(R.layout.test_login_activity, 0);
+    }
+
+    /**
+     * Launch test activity with give layout and parameter
+     */
+    protected CustomTestActivity launchActivity(int layoutId, int numViews) {
+        final Intent intent = new Intent(sContext, CustomTestActivity.class);
+        intent.putExtra(INTENT_EXTRA_LAYOUT_ID, layoutId);
+        intent.putExtra(INTENT_EXTRA_CUSTOM_VIEWS, numViews);
+        return mActivityRule.launchActivity(intent);
+    }
+
+    protected void finishActivity() {
+        try {
+            mActivityRule.finishActivity();
+        } catch (IllegalStateException e) {
+            // no op
+        }
+    }
+}
diff --git a/apct-tests/perftests/contentcapture/src/android/view/contentcapture/CustomTestActivity.java b/apct-tests/perftests/contentcapture/src/android/view/contentcapture/CustomTestActivity.java
new file mode 100644
index 0000000..e509837
--- /dev/null
+++ b/apct-tests/perftests/contentcapture/src/android/view/contentcapture/CustomTestActivity.java
@@ -0,0 +1,88 @@
+/*
+ * 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.view.contentcapture;
+
+import android.app.Activity;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.perftests.contentcapture.R;
+
+/**
+ * A simple activity used for testing, e.g. performance of activity switching, or as a base
+ * container of testing view.
+ */
+public class CustomTestActivity extends Activity {
+    public static final String INTENT_EXTRA_LAYOUT_ID = "layout_id";
+    public static final String INTENT_EXTRA_CUSTOM_VIEWS = "custom_view_number";
+    public static final int MAX_VIEWS = 500;
+    private static final int CUSTOM_CONTAINER_LAYOUT_ID = R.layout.test_container_activity;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        if (getIntent().hasExtra(INTENT_EXTRA_LAYOUT_ID)) {
+            final int layoutId = getIntent().getIntExtra(INTENT_EXTRA_LAYOUT_ID,
+                    /* defaultValue= */0);
+            setContentView(layoutId);
+            if (layoutId == CUSTOM_CONTAINER_LAYOUT_ID) {
+                createCustomViews(findViewById(R.id.root_view),
+                        getIntent().getIntExtra(INTENT_EXTRA_CUSTOM_VIEWS, MAX_VIEWS));
+            }
+        }
+    }
+
+    private void createCustomViews(LinearLayout root, int number) {
+        LinearLayout horizontalLayout = null;
+        for (int i = 0; i < number; i++) {
+            final int j = i % 8;
+            if (horizontalLayout != null && j == 0) {
+                root.addView(horizontalLayout);
+                horizontalLayout = null;
+            }
+            if (horizontalLayout == null) {
+                horizontalLayout = createHorizontalLayout();
+            }
+            horizontalLayout.addView(createItem(null, i));
+        }
+        if (horizontalLayout != null) {
+            root.addView(horizontalLayout);
+        }
+    }
+
+    private LinearLayout createHorizontalLayout() {
+        final LinearLayout layout = new LinearLayout(getApplicationContext());
+        layout.setOrientation(LinearLayout.HORIZONTAL);
+        return layout;
+    }
+
+    private LinearLayout createItem(Drawable drawable, int index) {
+        final LinearLayout group = new LinearLayout(getApplicationContext());
+        group.setOrientation(LinearLayout.VERTICAL);
+        group.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,
+                LinearLayout.LayoutParams.WRAP_CONTENT, /* weight= */ 1.0f));
+
+        final TextView text = new TextView(this);
+        text.setText("i = " + index);
+        group.addView(text);
+
+        return group;
+    }
+}
diff --git a/apct-tests/perftests/contentcapture/src/android/view/contentcapture/LoginTest.java b/apct-tests/perftests/contentcapture/src/android/view/contentcapture/LoginTest.java
new file mode 100644
index 0000000..750d3b4
--- /dev/null
+++ b/apct-tests/perftests/contentcapture/src/android/view/contentcapture/LoginTest.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.contentcapture;
+
+import static com.android.compatibility.common.util.ActivitiesWatcher.ActivityLifecycle.CREATED;
+import static com.android.compatibility.common.util.ActivitiesWatcher.ActivityLifecycle.DESTROYED;
+
+import android.perftests.utils.BenchmarkState;
+import android.view.View;
+
+import com.android.compatibility.common.util.ActivitiesWatcher.ActivityWatcher;
+import com.android.perftests.contentcapture.R;
+
+import org.junit.Test;
+
+public class LoginTest extends AbstractContentCapturePerfTestCase {
+
+    @Test
+    public void testLaunchActivity() throws Throwable {
+        enableService();
+
+        testActivityLaunchTime(R.layout.test_login_activity, 0);
+    }
+
+    @Test
+    public void testLaunchActivity_contain100Views() throws Throwable {
+        enableService();
+
+        testActivityLaunchTime(R.layout.test_container_activity, 100);
+    }
+
+    @Test
+    public void testLaunchActivity_contain300Views() throws Throwable {
+        enableService();
+
+        testActivityLaunchTime(R.layout.test_container_activity, 300);
+    }
+
+    @Test
+    public void testLaunchActivity_contain500Views() throws Throwable {
+        enableService();
+
+        testActivityLaunchTime(R.layout.test_container_activity, 500);
+    }
+
+    @Test
+    public void testLaunchActivity_noService() throws Throwable {
+        testActivityLaunchTime(R.layout.test_login_activity, 0);
+    }
+
+    @Test
+    public void testLaunchActivity_noService_contain100Views() throws Throwable {
+        testActivityLaunchTime(R.layout.test_container_activity, 100);
+    }
+
+    @Test
+    public void testLaunchActivity_noService_contain300Views() throws Throwable {
+        testActivityLaunchTime(R.layout.test_container_activity, 300);
+    }
+
+    @Test
+    public void testLaunchActivity_noService_contain500Views() throws Throwable {
+        testActivityLaunchTime(R.layout.test_container_activity, 500);
+    }
+
+    private void testActivityLaunchTime(int layoutId, int numViews) throws Throwable {
+        final ActivityWatcher watcher = startWatcher();
+
+        final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+        while (state.keepRunning()) {
+            launchActivity(layoutId, numViews);
+
+            // Ignore the time to finish the activity
+            state.pauseTiming();
+            watcher.waitFor(CREATED);
+            finishActivity();
+            watcher.waitFor(DESTROYED);
+            state.resumeTiming();
+        }
+    }
+
+    @Test
+    public void testOnVisibilityAggregated_visibleChanged() throws Throwable {
+        enableService();
+        final CustomTestActivity activity = launchActivity();
+        final View root = activity.getWindow().getDecorView();
+        final View username = root.findViewById(R.id.username);
+
+        testOnVisibilityAggregated(username);
+    }
+
+    @Test
+    public void testOnVisibilityAggregated_visibleChanged_noService() throws Throwable {
+        final CustomTestActivity activity = launchActivity();
+        final View root = activity.getWindow().getDecorView();
+        final View username = root.findViewById(R.id.username);
+
+        testOnVisibilityAggregated(username);
+    }
+
+    @Test
+    public void testOnVisibilityAggregated_visibleChanged_noOptions() throws Throwable {
+        enableService();
+        clearOptions();
+        final CustomTestActivity activity = launchActivity();
+        final View root = activity.getWindow().getDecorView();
+        final View username = root.findViewById(R.id.username);
+
+        testOnVisibilityAggregated(username);
+    }
+
+    @Test
+    public void testOnVisibilityAggregated_visibleChanged_notImportant() throws Throwable {
+        enableService();
+        final CustomTestActivity activity = launchActivity();
+        final View root = activity.getWindow().getDecorView();
+        final View username = root.findViewById(R.id.username);
+        username.setImportantForContentCapture(View.IMPORTANT_FOR_CONTENT_CAPTURE_NO);
+
+        testOnVisibilityAggregated(username);
+    }
+
+    private void testOnVisibilityAggregated(View view) throws Throwable {
+        BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+
+        while (state.keepRunning()) {
+            // Only count the time of onVisibilityAggregated()
+            state.pauseTiming();
+            mActivityRule.runOnUiThread(() -> {
+                state.resumeTiming();
+                view.onVisibilityAggregated(false);
+                state.pauseTiming();
+            });
+            mActivityRule.runOnUiThread(() -> {
+                state.resumeTiming();
+                view.onVisibilityAggregated(true);
+                state.pauseTiming();
+            });
+            state.resumeTiming();
+        }
+    }
+}
diff --git a/apct-tests/perftests/contentcapture/src/android/view/contentcapture/MyContentCaptureService.java b/apct-tests/perftests/contentcapture/src/android/view/contentcapture/MyContentCaptureService.java
new file mode 100644
index 0000000..b1dbb28
--- /dev/null
+++ b/apct-tests/perftests/contentcapture/src/android/view/contentcapture/MyContentCaptureService.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.contentcapture;
+
+import android.content.ComponentName;
+import android.service.contentcapture.ActivityEvent;
+import android.service.contentcapture.ContentCaptureService;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.Pair;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+public class MyContentCaptureService extends ContentCaptureService {
+
+    private static final String TAG = MyContentCaptureService.class.getSimpleName();
+    private static final String MY_PACKAGE = "com.android.perftests.contentcapture";
+    public static final String SERVICE_NAME = MY_PACKAGE + "/"
+            + MyContentCaptureService.class.getName();
+
+    private static ServiceWatcher sServiceWatcher;
+
+    @NonNull
+    public static ServiceWatcher setServiceWatcher() {
+        if (sServiceWatcher != null) {
+            throw new IllegalStateException("There Can Be Only One!");
+        }
+        sServiceWatcher = new ServiceWatcher();
+        return sServiceWatcher;
+    }
+
+    public static void resetStaticState() {
+        sServiceWatcher = null;
+    }
+
+    public static void clearServiceWatcher() {
+        if (sServiceWatcher != null) {
+            if (sServiceWatcher.mReadyToClear) {
+                sServiceWatcher.mService = null;
+                sServiceWatcher = null;
+            } else {
+                sServiceWatcher.mReadyToClear = true;
+            }
+        }
+    }
+
+    @Override
+    public void onConnected() {
+        Log.i(TAG, "onConnected: sServiceWatcher=" + sServiceWatcher);
+
+        if (sServiceWatcher == null) {
+            Log.e(TAG, "onConnected() without a watcher");
+            return;
+        }
+
+        if (!sServiceWatcher.mReadyToClear && sServiceWatcher.mService != null) {
+            Log.e(TAG, "onConnected(): already created: " + sServiceWatcher);
+            return;
+        }
+
+        sServiceWatcher.mService = this;
+        sServiceWatcher.mCreated.countDown();
+        sServiceWatcher.mReadyToClear = false;
+    }
+
+    @Override
+    public void onDisconnected() {
+        Log.i(TAG, "onDisconnected: sServiceWatcher=" + sServiceWatcher);
+
+        if (sServiceWatcher == null) {
+            Log.e(TAG, "onDisconnected() without a watcher");
+            return;
+        }
+        if (sServiceWatcher.mService == null) {
+            Log.e(TAG, "onDisconnected(): no service on " + sServiceWatcher);
+            return;
+        }
+
+        sServiceWatcher.mDestroyed.countDown();
+        clearServiceWatcher();
+    }
+
+    @Override
+    public void onCreateContentCaptureSession(ContentCaptureContext context,
+            ContentCaptureSessionId sessionId) {
+        Log.i(TAG, "onCreateContentCaptureSession(ctx=" + context + ", session=" + sessionId);
+    }
+
+    @Override
+    public void onDestroyContentCaptureSession(ContentCaptureSessionId sessionId) {
+        Log.i(TAG, "onDestroyContentCaptureSession(session=" + sessionId + ")");
+    }
+
+    @Override
+    public void onContentCaptureEvent(ContentCaptureSessionId sessionId,
+            ContentCaptureEvent event) {
+        Log.i(TAG, "onContentCaptureEventsRequest(session=" + sessionId + "): " + event);
+    }
+
+    @Override
+    public void onActivityEvent(ActivityEvent event) {
+        Log.i(TAG, "onActivityEvent(): " + event);
+    }
+
+    public static final class ServiceWatcher {
+
+        private static final long GENERIC_TIMEOUT_MS = 10_000;
+        private final CountDownLatch mCreated = new CountDownLatch(1);
+        private final CountDownLatch mDestroyed = new CountDownLatch(1);
+        private boolean mReadyToClear = true;
+        private Pair<Set<String>, Set<ComponentName>> mAllowList;
+
+        private MyContentCaptureService mService;
+
+        @NonNull
+        public MyContentCaptureService waitOnCreate() throws InterruptedException {
+            await(mCreated, "not created");
+
+            if (mService == null) {
+                throw new IllegalStateException("not created");
+            }
+
+            if (mAllowList != null) {
+                Log.d(TAG, "Allow after created: " + mAllowList);
+                mService.setContentCaptureWhitelist(mAllowList.first, mAllowList.second);
+            }
+
+            return mService;
+        }
+
+        public void waitOnDestroy() throws InterruptedException {
+            await(mDestroyed, "not destroyed");
+        }
+
+        /**
+         * Allow just this package.
+         */
+        public void setAllowSelf() {
+            final ArraySet<String> pkgs = new ArraySet<>(1);
+            pkgs.add(MY_PACKAGE);
+            mAllowList = new Pair<>(pkgs, null);
+        }
+
+        @Override
+        public String toString() {
+            return "mService: " + mService + " created: " + (mCreated.getCount() == 0)
+                    + " destroyed: " + (mDestroyed.getCount() == 0);
+        }
+
+        /**
+         * Awaits for a latch to be counted down.
+         */
+        private static void await(@NonNull CountDownLatch latch, @NonNull String fmt,
+                @Nullable Object... args)
+                throws InterruptedException {
+            final boolean called = latch.await(GENERIC_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+            if (!called) {
+                throw new IllegalStateException(String.format(fmt, args)
+                        + " in " + GENERIC_TIMEOUT_MS + "ms");
+            }
+        }
+    }
+}