Add Widget State Tracker

This change adds the state tracking portion of the detailed app jank
metrics feature. App Widgets will add/update/remove states throughout
their lifecycle; this change adds logic that will keep track of those
state changes. Several states could be active at once, when a state is
no longer active it will be moved to a previous state list.

The previously active states will eventually be processed and returned to
the state pool. States will be processed after each batch of JankData is
received. Currently JankData batches are received after 50 frames have
been rendered.

This change also adds unit tests that test the basic functionality of
the StateTracker. This includes adding/removing states as well as
retrieving states to process and returning processed states back to the
state pool.

Change-Id: I17a3f163f7b67d9451a9fe13c0d6520d2f658fb1
Bug: 368405795
Test: atest CoreAppJankTestCases
Flag: android.app.jank.detailed_app_jank_metrics_api
diff --git a/tests/AppJankTest/Android.bp b/tests/AppJankTest/Android.bp
new file mode 100644
index 0000000..acf8dc9
--- /dev/null
+++ b/tests/AppJankTest/Android.bp
@@ -0,0 +1,37 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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 {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test {
+    name: "CoreAppJankTestCases",
+    team: "trendy_team_system_performance",
+    srcs: ["src/**/*.java"],
+    static_libs: [
+        "androidx.test.rules",
+        "androidx.test.core",
+        "platform-test-annotations",
+        "flag-junit",
+    ],
+    platform_apis: true,
+    test_suites: ["device-tests"],
+    certificate: "platform",
+}
diff --git a/tests/AppJankTest/AndroidManifest.xml b/tests/AppJankTest/AndroidManifest.xml
new file mode 100644
index 0000000..ae97339
--- /dev/null
+++ b/tests/AppJankTest/AndroidManifest.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.app.jank.tests">
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+        <activity android:name=".EmptyActivity"
+                  android:label="EmptyActivity"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <action android:name="android.intent.action.VIEW_PERMISSION_USAGE"/>
+            </intent-filter>
+        </activity>
+    </application>
+
+    <!--  self-instrumenting test package. -->
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="android.app.jank.tests"
+        android:label="Core tests of App Jank Tracking">
+    </instrumentation>
+
+</manifest>
\ No newline at end of file
diff --git a/tests/AppJankTest/AndroidTest.xml b/tests/AppJankTest/AndroidTest.xml
new file mode 100644
index 0000000..c01c75c
--- /dev/null
+++ b/tests/AppJankTest/AndroidTest.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<configuration description="Config for Core App Jank Tests">
+    <option name="test-suite-tag" value="apct"/>
+
+    <option name="config-descriptor:metadata" key="component" value="systems"/>
+    <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
+    <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+    <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+    <option name="config-descriptor:metadata" key="parameter" value="no_foldable_states" />
+
+    <option name="not-shardable" value="true" />
+    <option name="install-arg" value="-t" />
+
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true"/>
+        <option name="test-file-name" value="CoreAppJankTestCases.apk"/>
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+        <option name="package" value="android.app.jank.tests"/>
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner"/>
+    </test>
+</configuration>
\ No newline at end of file
diff --git a/tests/AppJankTest/OWNERS b/tests/AppJankTest/OWNERS
new file mode 100644
index 0000000..806de57
--- /dev/null
+++ b/tests/AppJankTest/OWNERS
@@ -0,0 +1,4 @@
+steventerrell@google.com
+carmenjackson@google.com
+jjaggi@google.com
+pmuetschard@google.com
\ No newline at end of file
diff --git a/tests/AppJankTest/src/android/app/jank/tests/EmptyActivity.java b/tests/AppJankTest/src/android/app/jank/tests/EmptyActivity.java
new file mode 100644
index 0000000..b326765
--- /dev/null
+++ b/tests/AppJankTest/src/android/app/jank/tests/EmptyActivity.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.app.jank.tests;
+
+import android.app.Activity;
+
+public class EmptyActivity extends Activity {
+}
diff --git a/tests/AppJankTest/src/android/app/jank/tests/StateTrackerTest.java b/tests/AppJankTest/src/android/app/jank/tests/StateTrackerTest.java
new file mode 100644
index 0000000..541009e
--- /dev/null
+++ b/tests/AppJankTest/src/android/app/jank/tests/StateTrackerTest.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.app.jank.tests;
+
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.app.jank.Flags;
+import android.app.jank.StateTracker;
+import android.app.jank.StateTracker.StateData;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.view.Choreographer;
+
+import androidx.test.annotation.UiThreadTest;
+import androidx.test.core.app.ActivityScenario;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+
+
+@RunWith(AndroidJUnit4.class)
+public class StateTrackerTest {
+
+    private static final String WIDGET_CATEGORY_NONE = "None";
+    private static final String WIDGET_CATEGORY_SCROLL = "Scroll";
+    private static final String WIDGET_STATE_IDLE = "Idle";
+    private static final String WIDGET_STATE_SCROLLING = "Scrolling";
+    private StateTracker mStateTracker;
+    private Choreographer mChoreographer;
+
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+    /**
+     * Start an empty activity so choreographer won't return -1 for vsyncid.
+     */
+    private static ActivityScenario<EmptyActivity> sEmptyActivityRule;
+
+    @BeforeClass
+    public static void classSetup() {
+        sEmptyActivityRule = ActivityScenario.launch(EmptyActivity.class);
+    }
+
+    @AfterClass
+    public static void classTearDown() {
+        sEmptyActivityRule.close();
+    }
+
+    @Before
+    @UiThreadTest
+    public void setup() {
+        mChoreographer = Choreographer.getInstance();
+        mStateTracker = new StateTracker(mChoreographer);
+    }
+
+    /**
+     * Check that the start vsyncid is added when the state is first added and end vsyncid is
+     * set to the default value, indicating it has not been updated.
+     */
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API)
+    public void addWidgetState_VerifyStateHasStartVsyncId() {
+        mStateTracker.putState(WIDGET_CATEGORY_SCROLL, WIDGET_STATE_SCROLLING,
+                "addWidgetState_VerifyStateHasStartVsyncId");
+
+        ArrayList<StateData> stateList = new ArrayList<StateData>();
+        mStateTracker.retrieveAllStates(stateList);
+        StateData stateData = stateList.get(0);
+
+        assertTrue(stateData.mVsyncIdStart > 0);
+        assertTrue(stateData.mVsyncIdEnd == Long.MAX_VALUE);
+    }
+
+    /**
+     * Check that the end vsyncid is added when the state is removed.
+     */
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API)
+    public void removeWidgetState_VerifyStateHasEndVsyncId() {
+
+        mStateTracker.putState(WIDGET_CATEGORY_SCROLL, WIDGET_STATE_SCROLLING,
+                "removeWidgetState_VerifyStateHasEndVsyncId");
+        mStateTracker.removeState(WIDGET_CATEGORY_SCROLL, WIDGET_STATE_SCROLLING,
+                "removeWidgetState_VerifyStateHasEndVsyncId");
+
+        ArrayList<StateData> stateList = new ArrayList<StateData>();
+        mStateTracker.retrieveAllStates(stateList);
+        StateData stateData = stateList.get(0);
+
+        assertTrue(stateData.mVsyncIdStart > 0);
+        assertTrue(stateData.mVsyncIdEnd != Long.MAX_VALUE);
+    }
+
+    /**
+     * Check that duplicate states are aggregated into only one active instance.
+     */
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API)
+    public void addDuplicateStates_ConfirmStateCountOnlyOne() {
+        mStateTracker.putState(WIDGET_CATEGORY_SCROLL, WIDGET_STATE_SCROLLING,
+                "addDuplicateStates_ConfirmStateCountOnlyOne");
+
+        ArrayList<StateData> stateList = new ArrayList<>();
+        mStateTracker.retrieveAllStates(stateList);
+
+        assertEquals(stateList.size(), 1);
+
+        mStateTracker.putState(WIDGET_CATEGORY_SCROLL, WIDGET_STATE_SCROLLING,
+                "addDuplicateStates_ConfirmStateCountOnlyOne");
+
+        stateList.clear();
+
+        mStateTracker.retrieveAllStates(stateList);
+
+        assertEquals(stateList.size(), 1);
+    }
+
+    /**
+     * Check that correct distinct states are returned when all states are retrieved.
+     */
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API)
+    public void addThreeStateChanges_ConfirmThreeStatesReturned() {
+        mStateTracker.putState(WIDGET_CATEGORY_SCROLL, WIDGET_STATE_SCROLLING,
+                "addThreeStateChanges_ConfirmThreeStatesReturned");
+        mStateTracker.putState(WIDGET_CATEGORY_SCROLL, WIDGET_STATE_SCROLLING,
+                "addThreeStateChanges_ConfirmThreeStatesReturned_01");
+        mStateTracker.putState(WIDGET_CATEGORY_SCROLL, WIDGET_STATE_SCROLLING,
+                "addThreeStateChanges_ConfirmThreeStatesReturned_02");
+
+        ArrayList<StateData> stateList = new ArrayList<>();
+        mStateTracker.retrieveAllStates(stateList);
+
+        assertEquals(stateList.size(), 3);
+    }
+
+    /**
+     * Confirm when states are added and removed the removed states are moved to the previousStates
+     * list and returned when retrieveAllStates is called.
+     */
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API)
+    public void simulateAddingSeveralStates() {
+        for (int i = 0; i < 20; i++) {
+            mStateTracker.removeState(WIDGET_CATEGORY_SCROLL, WIDGET_STATE_SCROLLING,
+                    String.format("simulateAddingSeveralStates_%s", i - 1));
+            mStateTracker.putState(WIDGET_CATEGORY_SCROLL, WIDGET_STATE_SCROLLING,
+                    String.format("simulateAddingSeveralStates_%s", i));
+        }
+
+        ArrayList<StateData> stateList = new ArrayList<>();
+        mStateTracker.retrieveAllStates(stateList);
+
+        int countStatesWithEndVsync = 0;
+        for (int i = 0; i < stateList.size(); i++) {
+            if (stateList.get(i).mVsyncIdEnd != Long.MAX_VALUE) {
+                countStatesWithEndVsync++;
+            }
+        }
+
+        // The last state that was added would be an active state and should not have an associated
+        // end vsyncid.
+        assertEquals(19, countStatesWithEndVsync);
+    }
+
+    /**
+     * Confirm once a state has been attributed to a frame it has been removed from the previous
+     * state list.
+     */
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API)
+    public void confirmProcessedStates_RemovedFromPreviousStateList() {
+        for (int i = 0; i < 20; i++) {
+            mStateTracker.removeState(WIDGET_CATEGORY_SCROLL, WIDGET_STATE_SCROLLING,
+                    String.format("simulateAddingSeveralStates_%s", i - 1));
+            mStateTracker.putState(WIDGET_CATEGORY_SCROLL, WIDGET_STATE_SCROLLING,
+                    String.format("simulateAddingSeveralStates_%s", i));
+
+            if (i == 19) {
+                mStateTracker.removeState(WIDGET_CATEGORY_SCROLL, WIDGET_STATE_SCROLLING,
+                        String.format("simulateAddingSeveralStates_%s", i));
+            }
+        }
+
+        ArrayList<StateData> stateList = new ArrayList<>();
+        mStateTracker.retrieveAllStates(stateList);
+
+        assertEquals(20, stateList.size());
+
+        // Simulate processing all the states.
+        for (int i = 0; i < stateList.size(); i++) {
+            stateList.get(i).mProcessed = true;
+        }
+        // Clear out all processed states.
+        mStateTracker.stateProcessingComplete();
+
+        stateList.clear();
+
+        mStateTracker.retrieveAllStates(stateList);
+
+        assertEquals(0, stateList.size());
+    }
+}