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

Commit 0333c9a4 authored by Neil Gu's avatar Neil Gu Committed by Android (Google) Code Review
Browse files

Merge "Create ambient cue logger." into main

parents e2791825 b8c71eeb
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