Merge "Device Control metrics" into sc-dev

This commit is contained in:
Matt Pietal
2021-03-15 16:39:43 +00:00
committed by Android (Google) Code Review
7 changed files with 221 additions and 45 deletions

View File

@@ -0,0 +1,90 @@
/*
* 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
}
}

View File

@@ -0,0 +1,62 @@
/*
* 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
)
}
}

View File

@@ -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

View File

@@ -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 {
globalActionsComponent.handleShowGlobalActionsMenu()
}
if (wasLocked && activityContext == null) {
globalActionsComponent.handleShowGlobalActionsMenu()
} else {
action.invoke()
}

View File

@@ -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) {

View File

@@ -43,6 +43,7 @@ import android.widget.ListPopupWindow
import android.widget.Space
import android.widget.TextView
import com.android.systemui.R
import com.android.systemui.controls.ControlsMetricsLogger
import com.android.systemui.controls.ControlsServiceInfo
import com.android.systemui.controls.CustomIconCache
import com.android.systemui.controls.controller.ControlInfo
@@ -58,6 +59,7 @@ import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.globalactions.GlobalActionsPopupMenu
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.statusbar.phone.ShadeController
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.concurrency.DelayableExecutor
import dagger.Lazy
import java.text.Collator
@@ -77,7 +79,9 @@ class ControlsUiControllerImpl @Inject constructor (
val controlActionCoordinator: ControlActionCoordinator,
private val activityStarter: ActivityStarter,
private val shadeController: ShadeController,
private val iconCache: CustomIconCache
private val iconCache: CustomIconCache,
private val controlsMetricsLogger: ControlsMetricsLogger,
private val keyguardStateController: KeyguardStateController
) : ControlsUiController {
companion object {
@@ -133,7 +137,8 @@ class ControlsUiControllerImpl @Inject constructor (
return object : ControlsListingController.ControlsListingCallback {
override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) {
val lastItems = serviceInfos.map {
SelectionItem(it.loadLabel(), "", it.loadIcon(), it.componentName)
val uid = it.serviceInfo.applicationInfo.uid
SelectionItem(it.loadLabel(), "", it.loadIcon(), it.componentName, uid)
}
uiExecutor.execute {
parent.removeAllViews()
@@ -282,8 +287,19 @@ class ControlsUiControllerImpl @Inject constructor (
private fun showControlsView(items: List<SelectionItem>) {
controlViewsById.clear()
createListView()
createDropDown(items)
val itemsByComponent = items.associateBy { it.componentName }
val itemsWithStructure = mutableListOf<SelectionItem>()
allStructures.mapNotNullTo(itemsWithStructure) {
itemsByComponent.get(it.componentName)?.copy(structure = it.structure)
}
itemsWithStructure.sortWith(localeComparator)
val selectionItem = findSelectionItem(selectedStructure, itemsWithStructure) ?: items[0]
controlsMetricsLogger.refreshBegin(selectionItem.uid, !keyguardStateController.isUnlocked())
createListView(selectionItem)
createDropDown(itemsWithStructure, selectionItem)
createMenu()
}
@@ -325,22 +341,13 @@ class ControlsUiControllerImpl @Inject constructor (
})
}
private fun createDropDown(items: List<SelectionItem>) {
private fun createDropDown(items: List<SelectionItem>, selected: SelectionItem) {
items.forEach {
RenderInfo.registerComponentIcon(it.componentName, it.icon)
}
val itemsByComponent = items.associateBy { it.componentName }
val itemsWithStructure = mutableListOf<SelectionItem>()
allStructures.mapNotNullTo(itemsWithStructure) {
itemsByComponent.get(it.componentName)?.copy(structure = it.structure)
}
itemsWithStructure.sortWith(localeComparator)
val selectionItem = findSelectionItem(selectedStructure, itemsWithStructure) ?: items[0]
var adapter = ItemAdapter(context, R.layout.controls_spinner_item).apply {
addAll(itemsWithStructure)
addAll(items)
}
/*
@@ -349,13 +356,13 @@ class ControlsUiControllerImpl @Inject constructor (
* a similar effect
*/
val spinner = parent.requireViewById<TextView>(R.id.app_or_structure_spinner).apply {
setText(selectionItem.getTitle())
setText(selected.getTitle())
// override the default color on the dropdown drawable
(getBackground() as LayerDrawable).getDrawable(0)
.setTint(context.resources.getColor(R.color.control_spinner_dropdown, null))
}
if (itemsWithStructure.size == 1) {
if (items.size == 1) {
spinner.setBackground(null)
return
}
@@ -388,7 +395,7 @@ class ControlsUiControllerImpl @Inject constructor (
})
}
private fun createListView() {
private fun createListView(selected: SelectionItem) {
val inflater = LayoutInflater.from(context)
inflater.inflate(R.layout.controls_with_favorites, parent, true)
@@ -421,9 +428,11 @@ class ControlsUiControllerImpl @Inject constructor (
controlsController.get(),
uiExecutor,
bgExecutor,
controlActionCoordinator
controlActionCoordinator,
controlsMetricsLogger,
selected.uid
)
cvh.bindData(it)
cvh.bindData(it, false /* isLocked, will be ignored on initial load */)
controlViewsById.put(key, cvh)
}
}
@@ -528,6 +537,7 @@ class ControlsUiControllerImpl @Inject constructor (
}
override fun onRefreshState(componentName: ComponentName, controls: List<Control>) {
val isLocked = !keyguardStateController.isUnlocked()
controls.forEach { c ->
controlsById.get(ControlKey(componentName, c.getControlId()))?.let {
Log.d(ControlsUiController.TAG, "onRefreshState() for id: " + c.getControlId())
@@ -536,8 +546,8 @@ class ControlsUiControllerImpl @Inject constructor (
val key = ControlKey(componentName, c.getControlId())
controlsById.put(key, cws)
uiExecutor.execute {
controlViewsById.get(key)?.bindData(cws)
controlViewsById.get(key)?.let {
uiExecutor.execute { it.bindData(cws, isLocked) }
}
}
}
@@ -566,7 +576,8 @@ private data class SelectionItem(
val appName: CharSequence,
val structure: CharSequence,
val icon: Drawable,
val componentName: ComponentName
val componentName: ComponentName,
val uid: Int
) {
fun getTitle() = if (structure.isEmpty()) { appName } else { structure }
}

View File

@@ -19,6 +19,7 @@ package com.android.systemui.controls.ui
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.controls.ControlsMetricsLogger
import com.android.systemui.globalactions.GlobalActionsComponent
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.statusbar.policy.KeyguardStateController
@@ -63,6 +64,8 @@ class ControlActionCoordinatorImplTest : SysuiTestCase() {
private lateinit var taskViewFactory: Optional<TaskViewFactory>
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private lateinit var cvh: ControlViewHolder
@Mock
private lateinit var metricsLogger: ControlsMetricsLogger
companion object {
fun <T> any(): T = Mockito.any<T>()
@@ -86,7 +89,8 @@ class ControlActionCoordinatorImplTest : SysuiTestCase() {
globalActionsComponent,
taskViewFactory,
getFakeBroadcastDispatcher(),
lazyUiController
lazyUiController,
metricsLogger
))
`when`(cvh.cws.ci.controlId).thenReturn(ID)