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

Commit 4a05f9be authored by Darrell Shi's avatar Darrell Shi
Browse files

Log communal widgets snapshot

COMMUNAL_HUB_SNAPSHOT is a pulled atom which requires registering a
callback which gets called daily to log the current state of the widgets
in the Glanceable Hub, which includes first party widgets component
names, and total widget count.

Test: statsd_testdrive 10226
Test: atest CommunalMetricsLoggerTest
Test: atest CommunalMetricsStartableTest
Bug: 318712030
Flag: com.android.systemui.communal_hub
Change-Id: I1010b8b479e5115bccd8bdfd737488840e9a06c7
parent f6a6e121
Loading
Loading
Loading
Loading
+138 −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.communal

import android.app.StatsManager
import android.app.StatsManager.StatsPullAtomCallback
import android.content.pm.UserInfo
import android.platform.test.annotations.EnableFlags
import android.util.StatsEvent
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository
import com.android.systemui.communal.domain.interactor.communalInteractor
import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
import com.android.systemui.communal.shared.log.CommunalMetricsLogger
import com.android.systemui.concurrency.fakeExecutor
import com.android.systemui.flags.Flags.COMMUNAL_SERVICE_ENABLED
import com.android.systemui.flags.fakeFeatureFlagsClassic
import com.android.systemui.kosmos.testScope
import com.android.systemui.settings.fakeUserTracker
import com.android.systemui.shared.system.SysUiStatsLog
import com.android.systemui.testKosmos
import com.android.systemui.user.data.repository.fakeUserRepository
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
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.verify

@SmallTest
@EnableFlags(Flags.FLAG_COMMUNAL_HUB)
@RunWith(AndroidJUnit4::class)
class CommunalMetricsStartableTest : SysuiTestCase() {
    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope

    private val metricsLogger = mock<CommunalMetricsLogger>()
    private val statsManager = mock<StatsManager>()

    private val callbackCaptor = argumentCaptor<StatsPullAtomCallback>()

    private val userTracker = kosmos.fakeUserTracker
    private val userRepository = kosmos.fakeUserRepository
    private val widgetsRepository = kosmos.fakeCommunalWidgetRepository

    private lateinit var underTest: CommunalMetricsStartable

    @Before
    fun setUp() {
        kosmos.fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, true)

        // Set up an existing user, which is required for widgets to show
        val userInfos = listOf(UserInfo(0, "main", UserInfo.FLAG_MAIN))
        userRepository.setUserInfos(userInfos)
        userTracker.set(
            userInfos = userInfos,
            selectedUserIndex = 0,
        )

        underTest =
            CommunalMetricsStartable(
                kosmos.fakeExecutor,
                kosmos.communalSettingsInteractor,
                kosmos.communalInteractor,
                statsManager,
                metricsLogger,
            )
    }

    @Test
    fun start_communalFlagDisabled_doNotSetPullAtomCallback() {
        kosmos.fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, false)

        underTest.start()

        verify(statsManager, never()).setPullAtomCallback(anyInt(), anyOrNull(), any(), any())
    }

    @Test
    fun onPullAtom_atomTagDoesNotMatch_pullSkip() {
        underTest.start()

        verify(statsManager)
            .setPullAtomCallback(anyInt(), anyOrNull(), any(), callbackCaptor.capture())
        val callback = callbackCaptor.firstValue

        // Atom tag doesn't match COMMUNAL_HUB_SNAPSHOT
        val result =
            callback.onPullAtom(SysUiStatsLog.COMMUNAL_HUB_WIDGET_EVENT_REPORTED, mutableListOf())

        assertThat(result).isEqualTo(StatsManager.PULL_SKIP)
    }

    @Test
    fun onPullAtom_atomTagMatches_pullSuccess() =
        testScope.runTest {
            underTest.start()

            verify(statsManager)
                .setPullAtomCallback(anyInt(), anyOrNull(), any(), callbackCaptor.capture())
            val callback = callbackCaptor.firstValue

            // Populate some widgets
            widgetsRepository.addWidget(appWidgetId = 1, componentName = "pkg_1/cls_1")
            widgetsRepository.addWidget(appWidgetId = 2, componentName = "pkg_2/cls_2")

            val statsEvents = mutableListOf<StatsEvent>()
            val result = callback.onPullAtom(SysUiStatsLog.COMMUNAL_HUB_SNAPSHOT, statsEvents)

            verify(metricsLogger)
                .logWidgetsSnapshot(statsEvents, listOf("pkg_1/cls_1", "pkg_2/cls_2"))

            assertThat(result).isEqualTo(StatsManager.PULL_SUCCESS)
        }
}
+26 −0
Original line number Diff line number Diff line
@@ -16,11 +16,13 @@

package com.android.systemui.communal.log

import android.util.StatsEvent
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.communal.shared.log.CommunalMetricsLogger
import com.android.systemui.shared.system.SysUiStatsLog
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -90,4 +92,28 @@ class CommunalMetricsLoggerTest : SysuiTestCase() {
                2,
            )
    }

    @Test
    fun logWidgetsSnapshot_logOnlyLoggableComponents() {
        val statsEvents = mutableListOf<StatsEvent>()
        underTest.logWidgetsSnapshot(
            statsEvents,
            listOf(
                "com.blue.package/my_test_widget_1",
                "com.green.package/my_test_widget_2",
                "com.red.package/my_test_widget_3",
                "com.yellow.package/my_test_widget_4",
            ),
        )
        verify(statsLogProxy)
            .buildCommunalHubSnapshotStatsEvent(
                componentNames =
                    arrayOf(
                        "com.blue.package/my_test_widget_1",
                        "com.red.package/my_test_widget_3",
                    ),
                widgetCount = 4,
            )
        assertThat(statsEvents).hasSize(1)
    }
}
+72 −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.communal

import android.app.StatsManager
import android.util.StatsEvent
import com.android.systemui.CoreStartable
import com.android.systemui.communal.domain.interactor.CommunalInteractor
import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
import com.android.systemui.communal.shared.log.CommunalMetricsLogger
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.shared.system.SysUiStatsLog
import java.util.concurrent.Executor
import javax.inject.Inject
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking

@SysUISingleton
class CommunalMetricsStartable
@Inject
constructor(
    @Background private val bgExecutor: Executor,
    private val communalSettingsInteractor: CommunalSettingsInteractor,
    private val communalInteractor: CommunalInteractor,
    private val statsManager: StatsManager,
    private val metricsLogger: CommunalMetricsLogger,
) : CoreStartable, StatsManager.StatsPullAtomCallback {
    override fun start() {
        if (!communalSettingsInteractor.isCommunalFlagEnabled()) {
            return
        }

        statsManager.setPullAtomCallback(
            /* atomTag = */ SysUiStatsLog.COMMUNAL_HUB_SNAPSHOT,
            /* metadata = */ null,
            /* executor = */ bgExecutor,
            /* callback = */ this,
        )
    }

    override fun onPullAtom(atomTag: Int, statsEvents: MutableList<StatsEvent>): Int {
        if (atomTag != SysUiStatsLog.COMMUNAL_HUB_SNAPSHOT) {
            return StatsManager.PULL_SKIP
        }

        metricsLogger.logWidgetsSnapshot(
            statsEvents,
            componentNames =
                runBlocking {
                    communalInteractor.widgetContent.first().map {
                        it.componentName.flattenToString()
                    }
                },
        )
        return StatsManager.PULL_SUCCESS
    }
}
+6 −0
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ package com.android.systemui.communal.dagger
import com.android.systemui.CoreStartable
import com.android.systemui.communal.CommunalBackupRestoreStartable
import com.android.systemui.communal.CommunalDreamStartable
import com.android.systemui.communal.CommunalMetricsStartable
import com.android.systemui.communal.CommunalOngoingContentStartable
import com.android.systemui.communal.CommunalSceneStartable
import com.android.systemui.communal.log.CommunalLoggerStartable
@@ -59,4 +60,9 @@ interface CommunalStartableModule {
    @IntoMap
    @ClassKey(CommunalOngoingContentStartable::class)
    fun bindCommunalOngoingContentStartable(impl: CommunalOngoingContentStartable): CoreStartable

    @Binds
    @IntoMap
    @ClassKey(CommunalMetricsStartable::class)
    fun bindCommunalMetricsStartable(impl: CommunalMetricsStartable): CoreStartable
}
+32 −0
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.systemui.communal.shared.log

import android.util.StatsEvent
import com.android.systemui.communal.dagger.CommunalModule.Companion.LOGGABLE_PREFIXES
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.shared.system.SysUiStatsLog
@@ -55,6 +56,20 @@ constructor(
        )
    }

    /** Logs loggable widgets and the total widget count as a [StatsEvent]. */
    fun logWidgetsSnapshot(
        statsEvents: MutableList<StatsEvent>,
        componentNames: List<String>,
    ) {
        val loggableComponentNames = componentNames.filter { it.isLoggable() }.toTypedArray()
        statsEvents.add(
            statsLogProxy.buildCommunalHubSnapshotStatsEvent(
                componentNames = loggableComponentNames,
                widgetCount = componentNames.size,
            )
        )
    }

    /** Whether the component name matches any of the loggable prefixes. */
    private fun String.isLoggable(): Boolean {
        return loggablePrefixes.any { loggablePrefix -> startsWith(loggablePrefix) }
@@ -68,6 +83,12 @@ constructor(
            componentName: String,
            rank: Int,
        )

        /** Builds a [SysUiStatsLog.COMMUNAL_HUB_SNAPSHOT] stats event. */
        fun buildCommunalHubSnapshotStatsEvent(
            componentNames: Array<String>,
            widgetCount: Int,
        ): StatsEvent
    }
}

@@ -86,4 +107,15 @@ class CommunalStatsLogProxyImpl @Inject constructor() : CommunalMetricsLogger.St
            rank,
        )
    }

    override fun buildCommunalHubSnapshotStatsEvent(
        componentNames: Array<String>,
        widgetCount: Int,
    ): StatsEvent {
        return SysUiStatsLog.buildStatsEvent(
            SysUiStatsLog.COMMUNAL_HUB_SNAPSHOT,
            componentNames,
            widgetCount,
        )
    }
}