Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 9fc77b11 authored by Lucas Silva's avatar Lucas Silva
Browse files

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
parent 6b409d93
Loading
Loading
Loading
Loading
+254 −0
Original line number Diff line number Diff line
/*
 * 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)
    }
}
+269 −0
Original line number Diff line number Diff line
/*
 * 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)
    }
}
+28 −0
Original line number Diff line number Diff line
/*
 * 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
}
+43 −0
Original line number Diff line number Diff line
/*
 * 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(),
)
+98 −0
Original line number Diff line number Diff line
/*
 * 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)
    }
}
Loading