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

Commit b8c71eeb authored by Neil Gu's avatar Neil Gu
Browse files

Create ambient cue logger.

Bug: 423797861
Flag: com.android.systemui.enable_underlay
Test: atest AmbientCueRepositoryTest
Change-Id: I1d6932ec930d4983989ceac23cac8e7b442dbbb7
parent fc0d9e19
Loading
Loading
Loading
Loading
+62 −0
Original line number Diff line number Diff line
@@ -25,6 +25,7 @@ import android.app.smartspace.SmartspaceSession
import android.app.smartspace.SmartspaceSession.OnTargetsAvailableListener
import android.app.smartspace.SmartspaceTarget
import android.content.Intent
import android.content.applicationContext
import android.content.testableContext
import android.os.Binder
import android.os.Bundle
@@ -36,11 +37,15 @@ import androidx.test.filters.SmallTest
import com.android.systemui.LauncherProxyService
import com.android.systemui.LauncherProxyService.LauncherProxyListener
import com.android.systemui.SysuiTestCase
import com.android.systemui.ambientcue.data.logger.ambientCueLogger
import com.android.systemui.ambientcue.data.repository.AmbientCueRepositoryImpl.Companion.AMBIENT_CUE_SURFACE
import com.android.systemui.ambientcue.data.repository.AmbientCueRepositoryImpl.Companion.DEBOUNCE_DELAY_MS
import com.android.systemui.ambientcue.data.repository.AmbientCueRepositoryImpl.Companion.EXTRA_ACTION_TYPE
import com.android.systemui.ambientcue.data.repository.AmbientCueRepositoryImpl.Companion.EXTRA_ACTIVITY_ID
import com.android.systemui.ambientcue.data.repository.AmbientCueRepositoryImpl.Companion.EXTRA_ATTRIBUTION_DIALOG_PENDING_INTENT
import com.android.systemui.ambientcue.data.repository.AmbientCueRepositoryImpl.Companion.EXTRA_AUTOFILL_ID
import com.android.systemui.ambientcue.data.repository.AmbientCueRepositoryImpl.Companion.MA_ACTION_TYPE_NAME
import com.android.systemui.ambientcue.data.repository.AmbientCueRepositoryImpl.Companion.MR_ACTION_TYPE_NAME
import com.android.systemui.ambientcue.shared.model.ActionModel
import com.android.systemui.concurrency.fakeExecutor
import com.android.systemui.dump.DumpManager
@@ -99,6 +104,7 @@ class AmbientCueRepositoryTest : SysuiTestCase() {
            taskStackChangeListeners = kosmos.taskStackChangeListeners,
            backgroundDispatcher = kosmos.testDispatcher,
            secureSettingsRepository = kosmos.secureSettingsRepository,
            ambientCueLogger = kosmos.ambientCueLogger,
        )

    @Test
@@ -301,6 +307,36 @@ class AmbientCueRepositoryTest : SysuiTestCase() {
            verify(pendingIntent).send(any<Bundle>())
        }

    @Test
    fun action_ma_performMaLogger() =
        kosmos.runTest {
            val actions by collectLastValue(underTest.actions)
            runCurrent()
            verify(smartSpaceSession)
                .addOnTargetsAvailableListener(any(), onTargetsAvailableListenerCaptor.capture())
            onTargetsAvailableListenerCaptor.firstValue.onTargetsAvailable(listOf(maLoggerTarget))

            val action: ActionModel = actions!!.first()
            action.onPerformAction()
            runCurrent()
            verify(kosmos.ambientCueLogger).setFulfilledWithMaStatus()
        }

    @Test
    fun action_mr_performMrLogger() =
        kosmos.runTest {
            val actions by collectLastValue(underTest.actions)
            runCurrent()
            verify(smartSpaceSession)
                .addOnTargetsAvailableListener(any(), onTargetsAvailableListenerCaptor.capture())
            onTargetsAvailableListenerCaptor.firstValue.onTargetsAvailable(listOf(mrLoggerTarget))

            val action: ActionModel = actions!!.first()
            action.onPerformAction()
            runCurrent()
            verify(kosmos.ambientCueLogger).setFulfilledWithMrStatus()
        }

    @Test
    fun action_performLongClick_pendingIntentSent() =
        kosmos.runTest {
@@ -439,6 +475,32 @@ class AmbientCueRepositoryTest : SysuiTestCase() {
                            .build()
                    )
            }
        private val maLoggerTarget =
            mock<SmartspaceTarget> {
                on { smartspaceTargetId } doReturn AMBIENT_CUE_SURFACE
                on { actionChips } doReturn
                    listOf(
                        SmartspaceAction.Builder("action1-id", "title 1")
                            .setSubtitle("subtitle 1")
                            .setExtras(
                                Bundle().apply { putString(EXTRA_ACTION_TYPE, MA_ACTION_TYPE_NAME) }
                            )
                            .build()
                    )
            }
        private val mrLoggerTarget =
            mock<SmartspaceTarget> {
                on { smartspaceTargetId } doReturn AMBIENT_CUE_SURFACE
                on { actionChips } doReturn
                    listOf(
                        SmartspaceAction.Builder("action1-id", "title 1")
                            .setSubtitle("subtitle 1")
                            .setExtras(
                                Bundle().apply { putString(EXTRA_ACTION_TYPE, MR_ACTION_TYPE_NAME) }
                            )
                            .build()
                    )
            }

        private val launchIntent = Intent()
        private val intentTarget =
+4 −0
Original line number Diff line number Diff line
@@ -19,6 +19,8 @@ package com.android.systemui.ambientcue
import com.android.systemui.CoreStartable
import com.android.systemui.ambientcue.data.repository.AmbientCueRepository
import com.android.systemui.ambientcue.data.repository.AmbientCueRepositoryImpl
import com.android.systemui.ambientcue.data.logger.AmbientCueLogger
import com.android.systemui.ambientcue.data.logger.AmbientCueLoggerImpl
import com.android.systemui.ambientcue.ui.startable.AmbientCueCoreStartable
import dagger.Binds
import dagger.Module
@@ -34,4 +36,6 @@ interface AmbientCueModule {
    fun bindAmbientCueCoreStartable(startable: AmbientCueCoreStartable): CoreStartable

    @Binds fun bindsAmbientCueRepository(impl: AmbientCueRepositoryImpl): AmbientCueRepository

    @Binds fun bindsAmbientCueLogger(impl: AmbientCueLoggerImpl): AmbientCueLogger
}
+140 −0
Original line number Diff line number Diff line
/*
 * Copyright 2025 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.ambientcue.data.logger

import com.android.internal.util.FrameworkStatsLog
import com.android.systemui.util.time.SystemClock
import javax.inject.Inject

/**
 * Data object corresponding to a AmbientCueEventReported atom.
 *
 * Fields must be kept in sync with stats/atoms/ambientcue/amibent_cue_extension_atoms.proto.
 */
data class AmbientCueEventReported(
    var displayDurationMillis: Long = 0L,
    var fulfilledWithMaIntentMillis: Long = 0L,
    var fulfilledWithMrIntentMillis: Long = 0L,
    var loseFocusMillis: Long = 0L,
    var maCount: Int = 0,
    var mrCount: Int = 0,
    var fulfilledWithMaIntent: Boolean = false,
    var fulfilledWithMrIntent: Boolean = false,
    var clickedCloseButton: Boolean = false,
    var reachedTimeout: Boolean = false,
)

/**
 * Interface for writing AmbientCueEventReported atoms to statsd log. Use AmbientCueLoggerImpl in
 * production.
 */
interface AmbientCueLogger {
    /**
     * Sets AmbientCue display events.
     *
     * @param maCount Number of ma actions generated and displayed.
     * @param mrCount Number of mr actions suggestions generated and displayed.
     */
    fun setAmbientCueDisplayStatus(maCount: Int, mrCount: Int)

    /**
     * Sets AmbientCue lose focus time.
     *
     * @param loseFocusMillis The time in milliseconds that the cue bar lost focus.
     */
    fun setLoseFocusMillis(loseFocusMillis: Long)

    /** Sets fulfilled with ma intent events. */
    fun setFulfilledWithMaStatus()

    /** Sets fulfilled with mr intent events. */
    fun setFulfilledWithMrStatus()

    /** Sets clicked close button events. */
    fun setClickedCloseButtonStatus()

    /** Sets reached timeout events. */
    fun setReachedTimeoutStatus()

    /** Flushes a AmbientCueEventReported atom. */
    fun flushAmbientCueEventReported()

    /** Clears all saved status. */
    fun clear()
}

/** Implementation for logging UI events related to controls. */
class AmbientCueLoggerImpl @Inject constructor(private val systemClock: SystemClock) :
    AmbientCueLogger {
    private var report = AmbientCueEventReported()
    private var displayTimeMillis: Long = 0L

    /** {@see AmbientCueLogger#setAmbientCueDisplayStatus} */
    override fun setAmbientCueDisplayStatus(maCount: Int, mrCount: Int) {
        this.displayTimeMillis = systemClock.currentTimeMillis()
        report.maCount = maCount
        report.mrCount = mrCount
    }

    /** {@see AmbientCueLogger#setLoseFocusMillis} */
    override fun setLoseFocusMillis(loseFocusMillis: Long) {
        // TODO(b/425279501): Count loseFocusMillis.
    }

    /** {@see AmbientCueLogger#setFulfilledWithMaStatus} */
    override fun setFulfilledWithMaStatus() {
        // TODO(b/425279501): Count fulfilledWithMaIntentMillis.
        report.fulfilledWithMaIntent = true
    }

    /** {@see AmbientCueLogger#setFulfilledWithMrStatus} */
    override fun setFulfilledWithMrStatus() {
        // TODO(b/425279501): Count fulfilledWithMrIntentMillis.
        report.fulfilledWithMrIntent = true
    }

    override fun setClickedCloseButtonStatus() {
        // TODO(b/425279501): Add logic to set clickedCloseButton
    }

    override fun setReachedTimeoutStatus() {
        // TODO(b/425279501): Add logic to set reachedTimeout
    }

    /** {@see AmbientCueLogger#flushAmbientCueEventReported} */
    override fun flushAmbientCueEventReported() {
        FrameworkStatsLog.write(
            FrameworkStatsLog.AMBIENT_CUE_EVENT_REPORTED,
            report.displayDurationMillis,
            report.fulfilledWithMaIntentMillis,
            report.fulfilledWithMrIntentMillis,
            report.loseFocusMillis,
            report.maCount,
            report.mrCount,
            report.fulfilledWithMaIntent,
            report.fulfilledWithMrIntent,
            report.clickedCloseButton,
            report.reachedTimeout,
        )
    }

    /** {@see AmbientCueLogger#clear} */
    override fun clear() {
        report = AmbientCueEventReported()
        displayTimeMillis = 0L
    }
}
+33 −2
Original line number Diff line number Diff line
@@ -35,6 +35,7 @@ import androidx.tracing.trace
import com.android.systemui.Dumpable
import com.android.systemui.LauncherProxyService
import com.android.systemui.LauncherProxyService.LauncherProxyListener
import com.android.systemui.ambientcue.data.logger.AmbientCueLogger
import com.android.systemui.ambientcue.shared.model.ActionModel
import com.android.systemui.ambientcue.shared.model.IconModel
import com.android.systemui.dagger.SysUISingleton
@@ -114,6 +115,7 @@ constructor(
    private val taskStackChangeListeners: TaskStackChangeListeners,
    @Background backgroundDispatcher: CoroutineDispatcher,
    secureSettingsRepository: SecureSettingsRepository,
    private val ambientCueLogger: AmbientCueLogger,
) : AmbientCueRepository, Dumpable {

    init {
@@ -194,7 +196,13 @@ constructor(
                                                launchPendingIntent(pendingIntent)
                                            } else if (intent != null) {
                                                activityStarter.startActivity(intent, false)
                                            } else {}
                                        }
                                        if (actionType == MA_ACTION_TYPE_NAME) {
                                            ambientCueLogger.setFulfilledWithMaStatus()
                                        }
                                        if (actionType == MR_ACTION_TYPE_NAME) {
                                            ambientCueLogger.setFulfilledWithMrStatus()
                                        }
                                    },
                                    onPerformLongClick = {
@@ -300,6 +308,7 @@ constructor(
            )

    val targetTaskId: MutableStateFlow<Int> = MutableStateFlow(INVALID_TASK_ID)
    var isSessionStarted = false

    override val isAmbientCueEnabled: StateFlow<Boolean> =
        secureSettingsRepository
@@ -323,6 +332,26 @@ constructor(
                    !isDeactivated &&
                    globallyFocusedTaskId == targetTaskId.value
            }
            .onEach { isAttached ->
                if (isAttached && !isSessionStarted) {
                    isSessionStarted = true
                    var maCount = 0
                    var mrCount = 0
                    actions.value.forEach { action ->
                        when (action.actionType) {
                            MA_ACTION_TYPE_NAME -> maCount++
                            MR_ACTION_TYPE_NAME -> mrCount++
                            else -> {}
                        }
                    }
                    ambientCueLogger.setAmbientCueDisplayStatus(maCount, mrCount)
                }
                if (!isAttached && isSessionStarted) {
                    ambientCueLogger.flushAmbientCueEventReported()
                    ambientCueLogger.clear()
                    isSessionStarted = false
                }
            }
            .stateIn(
                scope = backgroundScope,
                started = SharingStarted.WhileSubscribed(),
@@ -350,7 +379,7 @@ constructor(
        @VisibleForTesting const val EXTRA_AUTOFILL_ID = "autofillId"
        @VisibleForTesting
        const val EXTRA_ATTRIBUTION_DIALOG_PENDING_INTENT = "attributionDialogPendingIntent"
        private const val EXTRA_ACTION_TYPE = "actionType"
        @VisibleForTesting const val EXTRA_ACTION_TYPE = "actionType"

        // Timeout to hide cuebar if it wasn't interacted with
        private const val TAG = "AmbientCueRepository"
@@ -360,5 +389,7 @@ constructor(
        @VisibleForTesting const val OPTED_IN = 0x10
        @VisibleForTesting const val OPTED_OUT = 0x01
        const val DEBOUNCE_DELAY_MS = 100L
        @VisibleForTesting const val MA_ACTION_TYPE_NAME = "ma"
        @VisibleForTesting const val MR_ACTION_TYPE_NAME = "mr"
    }
}
+2 −0
Original line number Diff line number Diff line
@@ -146,6 +146,7 @@ constructor(
    }

    fun hide() {
        // TODO(b/425279501) Log ambient cue close button click status.
        ambientCueInteractor.setDeactivated(true)
        isExpanded = false
    }
@@ -162,6 +163,7 @@ constructor(
        coroutineScopeTraced("AmbientCueViewModel") {
            deactivateCueBarJob = launch {
                delay(AMBIENT_CUE_TIMEOUT_SEC)
                // TODO(b/425279501) Log ambient cue timeout status.
                ambientCueInteractor.setDeactivated(true)
            }
        }
Loading