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

Commit 2451f55f authored by Matt Pietal's avatar Matt Pietal
Browse files

Device Control metrics

Log user events related to controls, such as when controls are
refreshed as well as user interactions with all controls (touch, long
press, drag)

Bug: 162066455
Test: manual
Change-Id: I32e0b560dcfb119f8a9eecd1dc7bc74de9253ca4
parent 8a80ffbe
Loading
Loading
Loading
Loading
+90 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.controls

import android.service.controls.DeviceTypes.DeviceType

import com.android.internal.logging.UiEvent
import com.android.internal.logging.UiEventLogger
import com.android.systemui.controls.ui.ControlViewHolder

/**
 * Interface for logging UI events related to controls
 */
interface ControlsMetricsLogger {

    /**
     * Assign a new instance id for this controls session, defined as when the controls area is
     * made visible to when it is closed.
     */
    fun assignInstanceId()

    fun touch(cvh: ControlViewHolder, isLocked: Boolean) {
        log(ControlsEvents.CONTROL_TOUCH.id, cvh.deviceType, cvh.uid, isLocked)
    }

    fun drag(cvh: ControlViewHolder, isLocked: Boolean) {
        log(ControlsEvents.CONTROL_DRAG.id, cvh.deviceType, cvh.uid, isLocked)
    }

    fun longPress(cvh: ControlViewHolder, isLocked: Boolean) {
        log(ControlsEvents.CONTROL_LONG_PRESS.id, cvh.deviceType, cvh.uid, isLocked)
    }

    fun refreshBegin(uid: Int, isLocked: Boolean) {
        assignInstanceId()
        log(ControlsEvents.CONTROL_REFRESH_BEGIN.id, 0, uid, isLocked)
    }

    fun refreshEnd(cvh: ControlViewHolder, isLocked: Boolean) {
        log(ControlsEvents.CONTROL_REFRESH_END.id, cvh.deviceType, cvh.uid, isLocked)
    }

    /**
     * Logs a controls-related event
     *
     * @param eventId Main UIEvent to capture
     * @param deviceType One of {@link android.service.controls.DeviceTypes}
     * @param packageName Package name of the service that receives the request
     * @param isLocked Is the device locked at the start of the action?
     */
    fun log(
        eventId: Int,
        @DeviceType deviceType: Int,
        uid: Int,
        isLocked: Boolean
    )

    private enum class ControlsEvents(val metricId: Int) : UiEventLogger.UiEventEnum {
        @UiEvent(doc = "User touched a control")
        CONTROL_TOUCH(714),

        @UiEvent(doc = "User dragged a control")
        CONTROL_DRAG(713),

        @UiEvent(doc = "User long-pressed a control")
        CONTROL_LONG_PRESS(715),

        @UiEvent(doc = "User has opened controls, and a state refresh has begun")
        CONTROL_REFRESH_BEGIN(716),

        @UiEvent(doc = "User has opened controls, and a state refresh has ended")
        CONTROL_REFRESH_END(717);

        override fun getId() = metricId
    }
}
+62 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.controls

import android.service.controls.DeviceTypes.DeviceType

import com.android.internal.logging.InstanceIdSequence
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.shared.system.SysUiStatsLog

import javax.inject.Inject

/**
 * Implementation for logging UI events related to controls
 */
@SysUISingleton
class ControlsMetricsLoggerImpl @Inject constructor() : ControlsMetricsLogger {

    companion object {
        private const val INSTANCE_ID_MAX = 1 shl 13
    }

    private val instanceIdSequence = InstanceIdSequence(INSTANCE_ID_MAX)
    private var instanceId = 0

    override fun assignInstanceId() {
        instanceId = instanceIdSequence.newInstanceId().id
    }

    /**
     * {@see ControlsMetricsLogger#log}
     */
    override fun log(
        eventId: Int,
        @DeviceType deviceType: Int,
        uid: Int,
        isLocked: Boolean
    ) {
        SysUiStatsLog.write(
            SysUiStatsLog.DEVICE_CONTROL_CHANGED,
            eventId,
            instanceId,
            deviceType,
            uid,
            isLocked
        )
    }
}
+5 −0
Original line number Diff line number Diff line
@@ -18,6 +18,8 @@ package com.android.systemui.controls.dagger

import android.app.Activity
import android.content.pm.PackageManager
import com.android.systemui.controls.ControlsMetricsLogger
import com.android.systemui.controls.ControlsMetricsLoggerImpl
import com.android.systemui.controls.controller.ControlsBindingController
import com.android.systemui.controls.controller.ControlsBindingControllerImpl
import com.android.systemui.controls.controller.ControlsController
@@ -79,6 +81,9 @@ abstract class ControlsModule {
    @Binds
    abstract fun provideUiController(controller: ControlsUiControllerImpl): ControlsUiController

    @Binds
    abstract fun provideMetricsLogger(logger: ControlsMetricsLoggerImpl): ControlsMetricsLogger

    @Binds
    abstract fun provideControlActionCoordinator(
        coordinator: ControlActionCoordinatorImpl
+14 −19
Original line number Diff line number Diff line
@@ -18,7 +18,6 @@ package com.android.systemui.controls.ui

import android.annotation.MainThread
import android.app.Dialog
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
@@ -33,6 +32,7 @@ import android.util.Log
import android.view.HapticFeedbackConstants
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.controls.ControlsMetricsLogger
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.globalactions.GlobalActionsComponent
@@ -54,13 +54,15 @@ class ControlActionCoordinatorImpl @Inject constructor(
    private val globalActionsComponent: GlobalActionsComponent,
    private val taskViewFactory: Optional<TaskViewFactory>,
    private val broadcastDispatcher: BroadcastDispatcher,
    private val lazyUiController: Lazy<ControlsUiController>
    private val lazyUiController: Lazy<ControlsUiController>,
    private val controlsMetricsLogger: ControlsMetricsLogger
) : ControlActionCoordinator {
    private var dialog: Dialog? = null
    private val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
    private var pendingAction: Action? = null
    private var actionsInProgress = mutableSetOf<String>()

    private val isLocked: Boolean
        get() = !keyguardStateController.isUnlocked()
    override var activityContext: Context? = null

    companion object {
@@ -73,6 +75,7 @@ class ControlActionCoordinatorImpl @Inject constructor(
    }

    override fun toggle(cvh: ControlViewHolder, templateId: String, isChecked: Boolean) {
        controlsMetricsLogger.touch(cvh, isLocked)
        bouncerOrRun(createAction(cvh.cws.ci.controlId, {
            cvh.layout.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK)
            cvh.action(BooleanAction(templateId, !isChecked))
@@ -80,6 +83,7 @@ class ControlActionCoordinatorImpl @Inject constructor(
    }

    override fun touch(cvh: ControlViewHolder, templateId: String, control: Control) {
        controlsMetricsLogger.touch(cvh, isLocked)
        val blockable = cvh.usePanel()
        bouncerOrRun(createAction(cvh.cws.ci.controlId, {
            cvh.layout.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK)
@@ -100,12 +104,14 @@ class ControlActionCoordinatorImpl @Inject constructor(
    }

    override fun setValue(cvh: ControlViewHolder, templateId: String, newValue: Float) {
        controlsMetricsLogger.drag(cvh, isLocked)
        bouncerOrRun(createAction(cvh.cws.ci.controlId, {
            cvh.action(FloatAction(templateId, newValue))
        }, false /* blockable */))
    }

    override fun longPress(cvh: ControlViewHolder) {
        controlsMetricsLogger.longPress(cvh, isLocked)
        bouncerOrRun(createAction(cvh.cws.ci.controlId, {
            // Long press snould only be called when there is valid control state, otherwise ignore
            cvh.cws.control?.let {
@@ -116,7 +122,7 @@ class ControlActionCoordinatorImpl @Inject constructor(
    }

    override fun runPendingAction(controlId: String) {
        if (!keyguardStateController.isUnlocked()) return
        if (isLocked) return
        if (pendingAction?.controlId == controlId) {
            pendingAction?.invoke()
            pendingAction = null
@@ -141,28 +147,17 @@ class ControlActionCoordinatorImpl @Inject constructor(
    @VisibleForTesting
    fun bouncerOrRun(action: Action) {
        if (keyguardStateController.isShowing()) {
            var closeDialog = !keyguardStateController.isUnlocked()
            if (closeDialog) {
            if (isLocked) {
                context.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))

                // pending actions will only run after the control state has been refreshed
                pendingAction = action
            }

            val wasLocked = isLocked
            activityStarter.dismissKeyguardThenExecute({
                Log.d(ControlsUiController.TAG, "Device unlocked, invoking controls action")
                if (closeDialog) {
                    activityContext?.let {
                        val i = Intent().apply {
                            component = ComponentName(context, ControlsActivity::class.java)
                            addFlags(
                                Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
                            putExtra(ControlsUiController.BACK_TO_GLOBAL_ACTIONS, false)
                        }
                        it.startActivity(i)
                    } ?: run {
                if (wasLocked && activityContext == null) {
                    globalActionsComponent.handleShowGlobalActionsMenu()
                    }
                } else {
                    action.invoke()
                }
+11 −2
Original line number Diff line number Diff line
@@ -49,6 +49,7 @@ import android.widget.TextView
import com.android.internal.graphics.ColorUtils
import com.android.systemui.Interpolators
import com.android.systemui.R
import com.android.systemui.controls.ControlsMetricsLogger
import com.android.systemui.controls.controller.ControlsController
import com.android.systemui.util.concurrency.DelayableExecutor
import kotlin.reflect.KClass
@@ -63,7 +64,9 @@ class ControlViewHolder(
    val controlsController: ControlsController,
    val uiExecutor: DelayableExecutor,
    val bgExecutor: DelayableExecutor,
    val controlActionCoordinator: ControlActionCoordinator
    val controlActionCoordinator: ControlActionCoordinator,
    val controlsMetricsLogger: ControlsMetricsLogger,
    val uid: Int
) {

    companion object {
@@ -141,7 +144,7 @@ class ControlViewHolder(
        status.setSelected(true)
    }

    fun bindData(cws: ControlWithState) {
    fun bindData(cws: ControlWithState, isLocked: Boolean) {
        // If an interaction is in progress, the update may visually interfere with the action the
        // action the user wants to make. Don't apply the update, and instead assume a new update
        // will coming from when the user interaction is complete.
@@ -171,10 +174,16 @@ class ControlViewHolder(
            controlActionCoordinator.runPendingAction(cws.ci.controlId)
        }

        val wasLoading = isLoading
        isLoading = false
        behavior = bindBehavior(behavior,
            findBehaviorClass(controlStatus, controlTemplate, deviceType))
        updateContentDescription()

        // Only log one event per control, at the moment we have determined that the control
        // switched from the loading to done state
        val doneLoading = wasLoading && !isLoading
        if (doneLoading) controlsMetricsLogger.refreshEnd(this, isLocked)
    }

    fun actionResponse(@ControlAction.ResponseResult response: Int) {
Loading