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,
+ )
+ }