Support querying UsageStatsManager in SystemUI

Adds a new repository and corresponding interactor to allow SystemUI to
query UsageStatsManager. Only activity events are supported.

Test: atest UsageStatsInteractorTest
Test: atest UsageStatsRepositoryTest
Flag: EXEMPT new code is unused
Bug: 350468769
Change-Id: I36ba083cb66ea146107123f8b5260ff8391b2483
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/usagestats/data/repository/UsageStatsRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/usagestats/data/repository/UsageStatsRepositoryTest.kt
new file mode 100644
index 0000000..81cdce4
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/usagestats/data/repository/UsageStatsRepositoryTest.kt
@@ -0,0 +1,254 @@
+/*
+ * 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 com.android.systemui.common.usagestats.data.repository
+
+import android.app.usage.UsageEvents
+import android.app.usage.UsageEventsQuery
+import android.app.usage.UsageStatsManager
+import android.os.UserHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.usagestats.data.model.UsageStatsQuery
+import com.android.systemui.common.usagestats.shared.model.ActivityEventModel
+import com.android.systemui.common.usagestats.shared.model.ActivityEventModel.Lifecycle
+import com.android.systemui.kosmos.backgroundCoroutineContext
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.mock
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class UsageStatsRepositoryTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+
+    private val fakeUsageStatsManager = FakeUsageStatsManager()
+
+    private val usageStatsManager =
+        mock<UsageStatsManager> {
+            on { queryEvents(any()) } doAnswer
+                { inv ->
+                    val query = inv.getArgument(0) as UsageEventsQuery
+                    fakeUsageStatsManager.queryEvents(query)
+                }
+        }
+
+    private val underTest by lazy {
+        UsageStatsRepositoryImpl(
+            bgContext = kosmos.backgroundCoroutineContext,
+            usageStatsManager = usageStatsManager,
+        )
+    }
+
+    @Test
+    fun testQueryWithBeginAndEndTime() =
+        testScope.runTest {
+            with(fakeUsageStatsManager) {
+                // This event is outside the queried time, and therefore should
+                // not be returned.
+                addEvent(
+                    type = UsageEvents.Event.ACTIVITY_RESUMED,
+                    timestamp = 5,
+                    instanceId = 1,
+                )
+                addEvent(
+                    type = UsageEvents.Event.ACTIVITY_PAUSED,
+                    timestamp = 10,
+                    instanceId = 1,
+                )
+                addEvent(
+                    type = UsageEvents.Event.ACTIVITY_STOPPED,
+                    timestamp = 20,
+                    instanceId = 2,
+                )
+                // This event is outside the queried time, and therefore should
+                // not be returned.
+                addEvent(
+                    type = UsageEvents.Event.ACTIVITY_DESTROYED,
+                    timestamp = 50,
+                    instanceId = 2,
+                )
+            }
+
+            assertThat(
+                    underTest.queryActivityEvents(
+                        UsageStatsQuery(MAIN_USER, startTime = 10, endTime = 50),
+                    ),
+                )
+                .containsExactly(
+                    ActivityEventModel(
+                        instanceId = 1,
+                        packageName = DEFAULT_PACKAGE,
+                        lifecycle = Lifecycle.PAUSED,
+                        timestamp = 10,
+                    ),
+                    ActivityEventModel(
+                        instanceId = 2,
+                        packageName = DEFAULT_PACKAGE,
+                        lifecycle = Lifecycle.STOPPED,
+                        timestamp = 20,
+                    ),
+                )
+        }
+
+    @Test
+    fun testQueryForDifferentUsers() =
+        testScope.runTest {
+            with(fakeUsageStatsManager) {
+                addEvent(
+                    user = MAIN_USER,
+                    type = UsageEvents.Event.ACTIVITY_PAUSED,
+                    timestamp = 10,
+                    instanceId = 1,
+                )
+                addEvent(
+                    user = SECONDARY_USER,
+                    type = UsageEvents.Event.ACTIVITY_RESUMED,
+                    timestamp = 11,
+                    instanceId = 2,
+                )
+            }
+
+            assertThat(
+                    underTest.queryActivityEvents(
+                        UsageStatsQuery(MAIN_USER, startTime = 10, endTime = 15),
+                    ),
+                )
+                .containsExactly(
+                    ActivityEventModel(
+                        instanceId = 1,
+                        packageName = DEFAULT_PACKAGE,
+                        lifecycle = Lifecycle.PAUSED,
+                        timestamp = 10,
+                    ),
+                )
+        }
+
+    @Test
+    fun testQueryForSpecificPackages() =
+        testScope.runTest {
+            with(fakeUsageStatsManager) {
+                addEvent(
+                    packageName = DEFAULT_PACKAGE,
+                    type = UsageEvents.Event.ACTIVITY_PAUSED,
+                    timestamp = 10,
+                    instanceId = 1,
+                )
+                addEvent(
+                    packageName = OTHER_PACKAGE,
+                    type = UsageEvents.Event.ACTIVITY_RESUMED,
+                    timestamp = 11,
+                    instanceId = 2,
+                )
+            }
+
+            assertThat(
+                    underTest.queryActivityEvents(
+                        UsageStatsQuery(
+                            MAIN_USER,
+                            startTime = 10,
+                            endTime = 10000,
+                            packageNames = listOf(OTHER_PACKAGE),
+                        ),
+                    ),
+                )
+                .containsExactly(
+                    ActivityEventModel(
+                        instanceId = 2,
+                        packageName = OTHER_PACKAGE,
+                        lifecycle = Lifecycle.RESUMED,
+                        timestamp = 11,
+                    ),
+                )
+        }
+
+    @Test
+    fun testNonActivityEvent() =
+        testScope.runTest {
+            with(fakeUsageStatsManager) {
+                addEvent(
+                    type = UsageEvents.Event.CHOOSER_ACTION,
+                    timestamp = 10,
+                    instanceId = 1,
+                )
+            }
+
+            assertThat(
+                    underTest.queryActivityEvents(
+                        UsageStatsQuery(
+                            MAIN_USER,
+                            startTime = 1,
+                            endTime = 20,
+                        ),
+                    ),
+                )
+                .isEmpty()
+        }
+
+    private class FakeUsageStatsManager() {
+        private val events = mutableMapOf<Int, MutableList<UsageEvents.Event>>()
+
+        fun queryEvents(query: UsageEventsQuery): UsageEvents {
+            val results =
+                events
+                    .getOrDefault(query.userId, emptyList())
+                    .filter { event ->
+                        query.packageNames.isEmpty() ||
+                            query.packageNames.contains(event.packageName)
+                    }
+                    .filter { event ->
+                        event.timeStamp in query.beginTimeMillis until query.endTimeMillis
+                    }
+                    .filter { event ->
+                        query.eventTypes.isEmpty() || query.eventTypes.contains(event.eventType)
+                    }
+            return UsageEvents(results, emptyArray())
+        }
+
+        fun addEvent(
+            type: Int,
+            instanceId: Int = 0,
+            user: UserHandle = MAIN_USER,
+            packageName: String = DEFAULT_PACKAGE,
+            timestamp: Long,
+        ) {
+            events
+                .getOrPut(user.identifier) { mutableListOf() }
+                .add(
+                    UsageEvents.Event(type, timestamp).apply {
+                        mPackage = packageName
+                        mInstanceId = instanceId
+                    }
+                )
+        }
+    }
+
+    private companion object {
+        const val DEFAULT_PACKAGE = "pkg.default"
+        const val OTHER_PACKAGE = "pkg.other"
+        val MAIN_USER: UserHandle = UserHandle.of(0)
+        val SECONDARY_USER: UserHandle = UserHandle.of(1)
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/usagestats/domain/interactor/UsageStatsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/usagestats/domain/interactor/UsageStatsInteractorTest.kt
new file mode 100644
index 0000000..af45588
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/usagestats/domain/interactor/UsageStatsInteractorTest.kt
@@ -0,0 +1,269 @@
+/*
+ * 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 com.android.systemui.common.usagestats.domain.interactor
+
+import android.annotation.CurrentTimeMillisLong
+import android.app.usage.UsageEvents
+import android.content.pm.UserInfo
+import android.os.UserHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.usagestats.data.repository.fakeUsageStatsRepository
+import com.android.systemui.common.usagestats.shared.model.ActivityEventModel
+import com.android.systemui.common.usagestats.shared.model.ActivityEventModel.Lifecycle
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.settings.fakeUserTracker
+import com.android.systemui.testKosmos
+import com.android.systemui.util.time.fakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class UsageStatsInteractorTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+
+    private val userTracker = kosmos.fakeUserTracker
+    private val systemClock = kosmos.fakeSystemClock
+    private val repository = kosmos.fakeUsageStatsRepository
+
+    private val underTest = kosmos.usageStatsInteractor
+
+    @Before
+    fun setUp() {
+        userTracker.set(listOf(MAIN_USER, SECONDARY_USER), 0)
+    }
+
+    @Test
+    fun testQueryWithBeginAndEndTime() =
+        testScope.runTest {
+            // This event is outside the queried time, and therefore should
+            // not be returned.
+            addEvent(
+                instanceId = 1,
+                type = UsageEvents.Event.ACTIVITY_RESUMED,
+                timestamp = 5,
+            )
+            addEvent(
+                type = UsageEvents.Event.ACTIVITY_PAUSED,
+                timestamp = 10,
+                instanceId = 1,
+            )
+            addEvent(
+                type = UsageEvents.Event.ACTIVITY_STOPPED,
+                timestamp = 20,
+                instanceId = 2,
+            )
+            // This event is outside the queried time, and therefore should
+            // not be returned.
+            addEvent(
+                type = UsageEvents.Event.ACTIVITY_DESTROYED,
+                timestamp = 50,
+                instanceId = 2,
+            )
+
+            assertThat(underTest.queryActivityEvents(startTime = 10, endTime = 50))
+                .containsExactly(
+                    ActivityEventModel(
+                        instanceId = 1,
+                        packageName = DEFAULT_PACKAGE,
+                        lifecycle = Lifecycle.PAUSED,
+                        timestamp = 10,
+                    ),
+                    ActivityEventModel(
+                        instanceId = 2,
+                        packageName = DEFAULT_PACKAGE,
+                        lifecycle = Lifecycle.STOPPED,
+                        timestamp = 20,
+                    ),
+                )
+        }
+
+    @Test
+    fun testQueryForDifferentUsers() =
+        testScope.runTest {
+            addEvent(
+                user = MAIN_USER.userHandle,
+                type = UsageEvents.Event.ACTIVITY_PAUSED,
+                timestamp = 10,
+                instanceId = 1,
+            )
+            addEvent(
+                user = SECONDARY_USER.userHandle,
+                type = UsageEvents.Event.ACTIVITY_RESUMED,
+                timestamp = 11,
+                instanceId = 2,
+            )
+
+            assertThat(underTest.queryActivityEvents(startTime = 10, endTime = 15))
+                .containsExactly(
+                    ActivityEventModel(
+                        instanceId = 1,
+                        packageName = DEFAULT_PACKAGE,
+                        lifecycle = Lifecycle.PAUSED,
+                        timestamp = 10,
+                    ),
+                )
+        }
+
+    @Test
+    fun testQueryWithUserSpecified() =
+        testScope.runTest {
+            addEvent(
+                user = MAIN_USER.userHandle,
+                type = UsageEvents.Event.ACTIVITY_PAUSED,
+                timestamp = 10,
+                instanceId = 1,
+            )
+            addEvent(
+                user = SECONDARY_USER.userHandle,
+                type = UsageEvents.Event.ACTIVITY_RESUMED,
+                timestamp = 11,
+                instanceId = 2,
+            )
+
+            assertThat(
+                    underTest.queryActivityEvents(
+                        startTime = 10,
+                        endTime = 15,
+                        userHandle = SECONDARY_USER.userHandle,
+                    ),
+                )
+                .containsExactly(
+                    ActivityEventModel(
+                        instanceId = 2,
+                        packageName = DEFAULT_PACKAGE,
+                        lifecycle = Lifecycle.RESUMED,
+                        timestamp = 11,
+                    ),
+                )
+        }
+
+    @Test
+    fun testQueryForSpecificPackages() =
+        testScope.runTest {
+            addEvent(
+                packageName = DEFAULT_PACKAGE,
+                type = UsageEvents.Event.ACTIVITY_PAUSED,
+                timestamp = 10,
+                instanceId = 1,
+            )
+            addEvent(
+                packageName = OTHER_PACKAGE,
+                type = UsageEvents.Event.ACTIVITY_RESUMED,
+                timestamp = 11,
+                instanceId = 2,
+            )
+
+            assertThat(
+                    underTest.queryActivityEvents(
+                        startTime = 10,
+                        endTime = 10000,
+                        packageNames = listOf(OTHER_PACKAGE),
+                    ),
+                )
+                .containsExactly(
+                    ActivityEventModel(
+                        instanceId = 2,
+                        packageName = OTHER_PACKAGE,
+                        lifecycle = Lifecycle.RESUMED,
+                        timestamp = 11,
+                    ),
+                )
+        }
+
+    @Test
+    fun testNonActivityEvent() =
+        testScope.runTest {
+            addEvent(
+                type = UsageEvents.Event.CHOOSER_ACTION,
+                timestamp = 10,
+                instanceId = 1,
+            )
+
+            assertThat(underTest.queryActivityEvents(startTime = 1, endTime = 20)).isEmpty()
+        }
+
+    @Test
+    fun testNoEndTimeSpecified() =
+        testScope.runTest {
+            systemClock.setCurrentTimeMillis(30)
+
+            addEvent(
+                type = UsageEvents.Event.ACTIVITY_PAUSED,
+                timestamp = 10,
+                instanceId = 1,
+            )
+            addEvent(
+                type = UsageEvents.Event.ACTIVITY_STOPPED,
+                timestamp = 20,
+                instanceId = 2,
+            )
+            // This event is outside the queried time, and therefore should
+            // not be returned.
+            addEvent(
+                type = UsageEvents.Event.ACTIVITY_DESTROYED,
+                timestamp = 50,
+                instanceId = 2,
+            )
+
+            assertThat(underTest.queryActivityEvents(startTime = 1))
+                .containsExactly(
+                    ActivityEventModel(
+                        instanceId = 1,
+                        packageName = DEFAULT_PACKAGE,
+                        lifecycle = Lifecycle.PAUSED,
+                        timestamp = 10,
+                    ),
+                    ActivityEventModel(
+                        instanceId = 2,
+                        packageName = DEFAULT_PACKAGE,
+                        lifecycle = Lifecycle.STOPPED,
+                        timestamp = 20,
+                    ),
+                )
+        }
+
+    private fun addEvent(
+        instanceId: Int,
+        user: UserHandle = MAIN_USER.userHandle,
+        packageName: String = DEFAULT_PACKAGE,
+        @UsageEvents.Event.EventType type: Int,
+        @CurrentTimeMillisLong timestamp: Long,
+    ) {
+        repository.addEvent(
+            instanceId = instanceId,
+            user = user,
+            packageName = packageName,
+            type = type,
+            timestamp = timestamp,
+        )
+    }
+
+    private companion object {
+        const val DEFAULT_PACKAGE = "pkg.default"
+        const val OTHER_PACKAGE = "pkg.other"
+        val MAIN_USER: UserInfo = UserInfo(0, "primary", UserInfo.FLAG_MAIN)
+        val SECONDARY_USER: UserInfo = UserInfo(10, "secondary", 0)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/common/usagestats/data/CommonUsageStatsDataLayerModule.kt b/packages/SystemUI/src/com/android/systemui/common/usagestats/data/CommonUsageStatsDataLayerModule.kt
new file mode 100644
index 0000000..3faa0dd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/common/usagestats/data/CommonUsageStatsDataLayerModule.kt
@@ -0,0 +1,28 @@
+/*
+ * 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 com.android.systemui.common.usagestats.data
+
+import com.android.systemui.common.usagestats.data.repository.UsageStatsRepository
+import com.android.systemui.common.usagestats.data.repository.UsageStatsRepositoryImpl
+import dagger.Binds
+import dagger.Module
+
+@Module
+abstract class CommonUsageStatsDataLayerModule {
+    @Binds
+    abstract fun bindUsageStatsRepository(impl: UsageStatsRepositoryImpl): UsageStatsRepository
+}
diff --git a/packages/SystemUI/src/com/android/systemui/common/usagestats/data/model/UsageStatsQuery.kt b/packages/SystemUI/src/com/android/systemui/common/usagestats/data/model/UsageStatsQuery.kt
new file mode 100644
index 0000000..270498c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/common/usagestats/data/model/UsageStatsQuery.kt
@@ -0,0 +1,43 @@
+/*
+ * 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 com.android.systemui.common.usagestats.data.model
+
+import android.annotation.CurrentTimeMillisLong
+import android.app.usage.UsageStatsManager
+import android.os.UserHandle
+import com.android.systemui.util.time.SystemClock
+
+/** Models a query which can be made to [UsageStatsManager] */
+data class UsageStatsQuery(
+    /** Specifies the user for the query. */
+    val user: UserHandle,
+    /**
+     * The inclusive beginning of the range of events to include. Defined in unix time, see
+     * [SystemClock.currentTimeMillis]
+     */
+    @CurrentTimeMillisLong val startTime: Long,
+    /**
+     * The exclusive end of the range of events to include. Defined in unix time, see
+     * [SystemClock.currentTimeMillis]
+     */
+    @CurrentTimeMillisLong val endTime: Long,
+    /**
+     * The list of package names to be included in the query. If empty, events for all packages will
+     * be queried.
+     */
+    val packageNames: List<String> = emptyList(),
+)
diff --git a/packages/SystemUI/src/com/android/systemui/common/usagestats/data/repository/UsageStatsRepository.kt b/packages/SystemUI/src/com/android/systemui/common/usagestats/data/repository/UsageStatsRepository.kt
new file mode 100644
index 0000000..e3f1174
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/common/usagestats/data/repository/UsageStatsRepository.kt
@@ -0,0 +1,98 @@
+/*
+ * 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 com.android.systemui.common.usagestats.data.repository
+
+import android.app.usage.UsageEvents
+import android.app.usage.UsageEventsQuery
+import android.app.usage.UsageStatsManager
+import com.android.app.tracing.coroutines.withContext
+import com.android.systemui.common.usagestats.data.model.UsageStatsQuery
+import com.android.systemui.common.usagestats.shared.model.ActivityEventModel
+import com.android.systemui.common.usagestats.shared.model.ActivityEventModel.Lifecycle
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+
+/** Repository for querying UsageStatsManager */
+interface UsageStatsRepository {
+    /** Query activity events. */
+    suspend fun queryActivityEvents(query: UsageStatsQuery): List<ActivityEventModel>
+}
+
+@SysUISingleton
+class UsageStatsRepositoryImpl
+@Inject
+constructor(
+    @Background private val bgContext: CoroutineContext,
+    private val usageStatsManager: UsageStatsManager,
+) : UsageStatsRepository {
+    private companion object {
+        const val TAG = "UsageStatsRepository"
+    }
+
+    override suspend fun queryActivityEvents(query: UsageStatsQuery): List<ActivityEventModel> =
+        withContext("$TAG#queryActivityEvents", bgContext) {
+            val systemQuery: UsageEventsQuery =
+                UsageEventsQuery.Builder(query.startTime, query.endTime)
+                    .apply {
+                        setUserId(query.user.identifier)
+                        setEventTypes(
+                            UsageEvents.Event.ACTIVITY_RESUMED,
+                            UsageEvents.Event.ACTIVITY_PAUSED,
+                            UsageEvents.Event.ACTIVITY_STOPPED,
+                            UsageEvents.Event.ACTIVITY_DESTROYED,
+                        )
+                        if (query.packageNames.isNotEmpty()) {
+                            setPackageNames(*query.packageNames.toTypedArray())
+                        }
+                    }
+                    .build()
+
+            val events: UsageEvents? = usageStatsManager.queryEvents(systemQuery)
+
+            buildList {
+                events.forEachEvent { event ->
+                    val lifecycle =
+                        when (event.eventType) {
+                            UsageEvents.Event.ACTIVITY_RESUMED -> Lifecycle.RESUMED
+                            UsageEvents.Event.ACTIVITY_PAUSED -> Lifecycle.PAUSED
+                            UsageEvents.Event.ACTIVITY_STOPPED -> Lifecycle.STOPPED
+                            UsageEvents.Event.ACTIVITY_DESTROYED -> Lifecycle.DESTROYED
+                            else -> Lifecycle.UNKNOWN
+                        }
+
+                    add(
+                        ActivityEventModel(
+                            instanceId = event.instanceId,
+                            packageName = event.packageName,
+                            lifecycle = lifecycle,
+                            timestamp = event.timeStamp,
+                        )
+                    )
+                }
+            }
+        }
+}
+
+private inline fun UsageEvents?.forEachEvent(action: (UsageEvents.Event) -> Unit) {
+    this ?: return
+    val event = UsageEvents.Event()
+    while (getNextEvent(event)) {
+        action(event)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/common/usagestats/domain/UsageStatsInteractor.kt b/packages/SystemUI/src/com/android/systemui/common/usagestats/domain/UsageStatsInteractor.kt
new file mode 100644
index 0000000..81848e2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/common/usagestats/domain/UsageStatsInteractor.kt
@@ -0,0 +1,59 @@
+/*
+ * 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 com.android.systemui.common.usagestats.domain
+
+import android.annotation.CurrentTimeMillisLong
+import android.os.UserHandle
+import com.android.systemui.common.usagestats.data.model.UsageStatsQuery
+import com.android.systemui.common.usagestats.data.repository.UsageStatsRepository
+import com.android.systemui.common.usagestats.shared.model.ActivityEventModel
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.util.time.SystemClock
+import javax.inject.Inject
+
+@SysUISingleton
+class UsageStatsInteractor
+@Inject
+constructor(
+    private val userTracker: UserTracker,
+    private val repository: UsageStatsRepository,
+    private val systemClock: SystemClock,
+) {
+    suspend fun queryActivityEvents(
+        @CurrentTimeMillisLong startTime: Long,
+        @CurrentTimeMillisLong endTime: Long = systemClock.currentTimeMillis(),
+        userHandle: UserHandle = UserHandle.CURRENT,
+        packageNames: List<String> = emptyList(),
+    ): List<ActivityEventModel> {
+        val user =
+            if (userHandle == UserHandle.CURRENT) {
+                userTracker.userHandle
+            } else {
+                userHandle
+            }
+
+        return repository.queryActivityEvents(
+            UsageStatsQuery(
+                startTime = startTime,
+                endTime = endTime,
+                user = user,
+                packageNames = packageNames,
+            )
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/common/usagestats/shared/model/ActivityEventModel.kt b/packages/SystemUI/src/com/android/systemui/common/usagestats/shared/model/ActivityEventModel.kt
new file mode 100644
index 0000000..9ef33fa
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/common/usagestats/shared/model/ActivityEventModel.kt
@@ -0,0 +1,41 @@
+/*
+ * 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 com.android.systemui.common.usagestats.shared.model
+
+import android.annotation.CurrentTimeMillisLong
+import android.app.Activity
+import com.android.systemui.util.time.SystemClock
+
+/** Represents [Activity] lifecycle events. */
+data class ActivityEventModel(
+    /** Uniquely identifies an activity. */
+    val instanceId: Int,
+    /** The package name of the source of this event. */
+    val packageName: String,
+    /** The lifecycle change which this event represents. */
+    val lifecycle: Lifecycle,
+    /** The timestamp of the event. Defined in unix time, see [SystemClock.currentTimeMillis] */
+    @CurrentTimeMillisLong val timestamp: Long,
+) {
+    enum class Lifecycle {
+        UNKNOWN,
+        RESUMED,
+        PAUSED,
+        STOPPED,
+        DESTROYED
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
index 2ea27b7..21a704d 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
@@ -42,6 +42,7 @@
 import android.app.role.RoleManager;
 import android.app.smartspace.SmartspaceManager;
 import android.app.trust.TrustManager;
+import android.app.usage.UsageStatsManager;
 import android.appwidget.AppWidgetManager;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothManager;
@@ -544,6 +545,13 @@
         return context.getSystemService(UiModeManager.class);
     }
 
+    /** */
+    @Provides
+    @Singleton
+    static UsageStatsManager provideUsageStatsManager(Context context) {
+        return context.getSystemService(UsageStatsManager.class);
+    }
+
     @Provides
     @Main
     static Resources provideResources(Context context) {
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 25b6b14..0363a68 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -48,6 +48,7 @@
 import com.android.systemui.classifier.FalsingModule;
 import com.android.systemui.clipboardoverlay.dagger.ClipboardOverlayModule;
 import com.android.systemui.common.data.CommonDataLayerModule;
+import com.android.systemui.common.usagestats.data.CommonUsageStatsDataLayerModule;
 import com.android.systemui.communal.dagger.CommunalModule;
 import com.android.systemui.complication.dagger.ComplicationComponent;
 import com.android.systemui.controls.dagger.ControlsModule;
@@ -205,6 +206,7 @@
         ClockRegistryModule.class,
         CommunalModule.class,
         CommonDataLayerModule.class,
+        CommonUsageStatsDataLayerModule.class,
         ConfigurationControllerModule.class,
         ConnectivityModule.class,
         ControlsModule.class,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/common/usagestats/data/repository/FakeUsageStatsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/common/usagestats/data/repository/FakeUsageStatsRepository.kt
new file mode 100644
index 0000000..d73de76
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/common/usagestats/data/repository/FakeUsageStatsRepository.kt
@@ -0,0 +1,72 @@
+/*
+ * 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 com.android.systemui.common.usagestats.data.repository
+
+import android.annotation.CurrentTimeMillisLong
+import android.app.usage.UsageEvents
+import android.os.UserHandle
+import com.android.systemui.common.usagestats.data.model.UsageStatsQuery
+import com.android.systemui.common.usagestats.shared.model.ActivityEventModel
+import com.android.systemui.common.usagestats.shared.model.ActivityEventModel.Lifecycle
+
+class FakeUsageStatsRepository : UsageStatsRepository {
+    private val events = mutableMapOf<UserHandle, MutableList<UsageEvents.Event>>()
+
+    override suspend fun queryActivityEvents(query: UsageStatsQuery): List<ActivityEventModel> {
+        return events
+            .getOrDefault(query.user, emptyList())
+            .filter { event ->
+                query.packageNames.isEmpty() || query.packageNames.contains(event.packageName)
+            }
+            .filter { event -> event.timeStamp in query.startTime until query.endTime }
+            .filter { event -> event.eventType.toActivityLifecycle() != Lifecycle.UNKNOWN }
+            .map { event ->
+                ActivityEventModel(
+                    instanceId = event.instanceId,
+                    packageName = event.packageName,
+                    lifecycle = event.eventType.toActivityLifecycle(),
+                    timestamp = event.timeStamp,
+                )
+            }
+    }
+
+    fun addEvent(
+        instanceId: Int,
+        user: UserHandle,
+        packageName: String,
+        @UsageEvents.Event.EventType type: Int,
+        @CurrentTimeMillisLong timestamp: Long,
+    ) {
+        events
+            .getOrPut(user) { mutableListOf() }
+            .add(
+                UsageEvents.Event(type, timestamp).apply {
+                    mPackage = packageName
+                    mInstanceId = instanceId
+                }
+            )
+    }
+}
+
+private fun Int.toActivityLifecycle(): Lifecycle =
+    when (this) {
+        UsageEvents.Event.ACTIVITY_RESUMED -> Lifecycle.RESUMED
+        UsageEvents.Event.ACTIVITY_PAUSED -> Lifecycle.PAUSED
+        UsageEvents.Event.ACTIVITY_STOPPED -> Lifecycle.STOPPED
+        UsageEvents.Event.ACTIVITY_DESTROYED -> Lifecycle.DESTROYED
+        else -> Lifecycle.UNKNOWN
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/common/usagestats/data/repository/UsageStatsRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/common/usagestats/data/repository/UsageStatsRepositoryKosmos.kt
new file mode 100644
index 0000000..7dac59e
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/common/usagestats/data/repository/UsageStatsRepositoryKosmos.kt
@@ -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 com.android.systemui.common.usagestats.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.usageStatsRepository: UsageStatsRepository by Kosmos.Fixture { fakeUsageStatsRepository }
+val Kosmos.fakeUsageStatsRepository by Kosmos.Fixture { FakeUsageStatsRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/common/usagestats/domain/interactor/UsageStatsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/common/usagestats/domain/interactor/UsageStatsInteractorKosmos.kt
new file mode 100644
index 0000000..06a680f
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/common/usagestats/domain/interactor/UsageStatsInteractorKosmos.kt
@@ -0,0 +1,32 @@
+/*
+ * 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 com.android.systemui.common.usagestats.domain.interactor
+
+import com.android.systemui.common.usagestats.data.repository.usageStatsRepository
+import com.android.systemui.common.usagestats.domain.UsageStatsInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.settings.userTracker
+import com.android.systemui.util.time.fakeSystemClock
+
+val Kosmos.usageStatsInteractor: UsageStatsInteractor by
+    Kosmos.Fixture {
+        UsageStatsInteractor(
+            userTracker = userTracker,
+            repository = usageStatsRepository,
+            systemClock = fakeSystemClock,
+        )
+    }