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

Commit e932d31a authored by Lucas Silva's avatar Lucas Silva Committed by Android (Google) Code Review
Browse files

Merge "Support querying UsageStatsManager in SystemUI" into main

parents 75a61eba 9fc77b11
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