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/core/java/android/app/jank/StateTracker.java b/core/java/android/app/jank/StateTracker.java
new file mode 100644
index 0000000..cb457ff
--- /dev/null
+++ b/core/java/android/app/jank/StateTracker.java
@@ -0,0 +1,206 @@
+/*
+ * 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;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.util.Pools.SimplePool;
+import android.view.Choreographer;
+
+import androidx.annotation.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * StateTracker is responsible for keeping track of currently active states as well as
+ * previously encountered states. States are added, updated or removed by widgets that support state
+ * tracking. When a state is first added it will get a vsyncid associated to it, when that state
+ * is removed or updated to a different state it will have a second vsyncid associated with it. The
+ * two vsyncids create a range of ids where that particular state was active.
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_DETAILED_APP_JANK_METRICS_API)
+@VisibleForTesting
+public class StateTracker {
+
+    // Used to synchronize access to mPreviousStates.
+    private final Object mLock = new Object();
+    private Choreographer mChoreographer;
+
+    // The max number of StateData objects that will be stored in the pool for reuse.
+    private static final int MAX_POOL_SIZE = 500;
+    // The max number of currently active states to track.
+    protected static final int MAX_CONCURRENT_STATE_COUNT = 25;
+    // The maximum number of previously seen states that will be counted.
+    protected static final int MAX_PREVIOUSLY_ACTIVE_STATE_COUNT = 1000;
+
+    // Pool to store the previously used StateData objects to save recreating them each time.
+    private final SimplePool<StateData> mStateDataObjectPool = new SimplePool<>(MAX_POOL_SIZE);
+    // Previously encountered states that have not been associated to a frame.
+    private ArrayList<StateData> mPreviousStates = new ArrayList<>();
+    // Currently active widgets and widget states
+    private ConcurrentHashMap<String, StateData> mActiveStates = new ConcurrentHashMap<>();
+
+    public StateTracker(@NonNull Choreographer choreographer) {
+        mChoreographer = choreographer;
+    }
+
+    /**
+     * Updates the currentState to the nextState.
+     * @param widgetCategory preselected general widget category.
+     * @param widgetId developer defined widget id if available.
+     * @param currentState current state of the widget.
+     * @param nextState the state the widget will be in.
+     */
+    public void updateState(@NonNull String widgetCategory, @NonNull String widgetId,
+            @NonNull String currentState, @NonNull String nextState) {
+        // remove the now inactive state from the active states list
+        removeState(widgetCategory, widgetId, currentState);
+
+        // add the updated state to the active states list
+        putState(widgetCategory, widgetId, nextState);
+    }
+
+    /**
+     * Removes the state from the active state list and adds it to the previously encountered state
+     * list. Associates an end vsync id to the state.
+     * @param widgetCategory preselected general widget category.
+     * @param widgetId developer defined widget id if available.
+     * @param widgetState no longer active widget state.
+     */
+    public void removeState(@NonNull String widgetCategory, @NonNull String widgetId,
+            @NonNull String widgetState) {
+
+        String stateKey = getStateKey(widgetCategory, widgetId, widgetState);
+        // Check if we have the active state
+        StateData stateData = mActiveStates.remove(stateKey);
+
+        // If there are no states that match just return.
+        // This can happen if mActiveStates is at MAX_CONCURRENT_STATE_COUNT and a widget tries to
+        // remove a state that was never added or if a widget tries to remove the same state twice.
+        if (stateData == null) return;
+
+        synchronized (mLock) {
+            stateData.mVsyncIdEnd = mChoreographer.getVsyncId();
+            // Add the StateData to the previous state list.  We  need to keep a list of all the
+            // previously active states until we can process the next batch of frame data.
+            if (mPreviousStates.size() < MAX_PREVIOUSLY_ACTIVE_STATE_COUNT) {
+                mPreviousStates.add(stateData);
+            }
+        }
+    }
+
+    /**
+     * Adds a new state to the active state list. Associates a start vsync id to the state.
+     * @param widgetCategory preselected general widget category.
+     * @param widgetId developer defined widget id if available.
+     * @param widgetState the current active widget state.
+     */
+    public void putState(@NonNull String widgetCategory, @NonNull String widgetId,
+            @NonNull String widgetState) {
+
+        // Check if we can accept a new state
+        if (mActiveStates.size() >= MAX_CONCURRENT_STATE_COUNT) return;
+
+        String stateKey = getStateKey(widgetCategory, widgetId, widgetState);
+
+        // Check if there is currently any active states
+        // if there is already a state that matches then its presumed as still active.
+        if (mActiveStates.containsKey(stateKey)) return;
+
+        // Check if we have am unused state object in the pool
+        StateData stateData = mStateDataObjectPool.acquire();
+        if (stateData == null) {
+            stateData = new StateData();
+        }
+        stateData.mVsyncIdStart = mChoreographer.getVsyncId();
+        stateData.mStateDataKey = stateKey;
+        stateData.mWidgetState = widgetState;
+        stateData.mWidgetCategory = widgetCategory;
+        stateData.mWidgetId = widgetId;
+        stateData.mVsyncIdEnd = Long.MAX_VALUE;
+        mActiveStates.put(stateKey, stateData);
+
+    }
+
+    /**
+     * Will add all previously encountered states as well as all currently active states to the list
+     * that was passed in.
+     * @param allStates the list that will be populated with the widget states.
+     */
+    public void retrieveAllStates(ArrayList<StateData> allStates) {
+        synchronized (mLock) {
+            allStates.addAll(mPreviousStates);
+            allStates.addAll(mActiveStates.values());
+        }
+    }
+
+    /**
+     * Call after processing a batch of JankData, will remove any processed states from the
+     * previous state list.
+     */
+    public void stateProcessingComplete() {
+        synchronized (mLock) {
+            for (int i = mPreviousStates.size() - 1; i >= 0; i--) {
+                StateData stateData = mPreviousStates.get(i);
+                if (stateData.mProcessed) {
+                    mPreviousStates.remove(stateData);
+                    mStateDataObjectPool.release(stateData);
+                }
+            }
+        }
+    }
+
+    /**
+     * Only intended to be used for testing, this enables test methods to submit pending states
+     * with known start and end vsyncids.  This allows testing methods to know the exact ranges
+     * of vysncid and calculate exactly how many states should or should not be processed.
+     * @param stateData the data that will be added.
+     *
+     */
+    @VisibleForTesting
+    public void addPendingStateData(List<StateData> stateData) {
+        synchronized (mLock) {
+            mPreviousStates.addAll(stateData);
+        }
+    }
+
+    private String getStateKey(String widgetCategory, String widgetId, String widgetState) {
+        return widgetCategory + widgetId + widgetState;
+    }
+
+    /**
+     * @hide
+     */
+    @VisibleForTesting
+    public static class StateData {
+
+        // Concatenated string of widget category, widget state and widget id.
+        public String mStateDataKey;
+        public String mWidgetCategory;
+        public String mWidgetState;
+        public String mWidgetId;
+        // vsyncid when the state was first added.
+        public long mVsyncIdStart;
+        // vsyncid for when the state was removed.
+        public long mVsyncIdEnd;
+        // Used to indicate whether this state has been processed and can be returned to the pool.
+        public boolean mProcessed;
+    }
+}
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());
+    }
+}