diff --git a/packages/SystemUI/src/com/android/systemui/controls/ControlsMetricsLogger.kt b/packages/SystemUI/src/com/android/systemui/controls/ControlsMetricsLogger.kt new file mode 100644 index 0000000000000..3bfdcae2017b8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/controls/ControlsMetricsLogger.kt @@ -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 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/controls/ControlsMetricsLoggerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ControlsMetricsLoggerImpl.kt new file mode 100644 index 0000000000000..c1656327198b2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/controls/ControlsMetricsLoggerImpl.kt @@ -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 + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt index ed625de9dce89..a165bb2c954fa 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsModule.kt @@ -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 diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt index 58a5981845c79..477c22068851d 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt @@ -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, private val broadcastDispatcher: BroadcastDispatcher, - private val lazyUiController: Lazy + private val lazyUiController: Lazy, + 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() - + 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() } diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt index 9d92a40beec47..3e02890221623 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt @@ -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) { diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt index 0f7f48ff951a1..d08882b1dbd2b 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt @@ -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) { 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) { controlViewsById.clear() - createListView() - createDropDown(items) + val itemsByComponent = items.associateBy { it.componentName } + val itemsWithStructure = mutableListOf() + 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) { + private fun createDropDown(items: List, selected: SelectionItem) { items.forEach { RenderInfo.registerComponentIcon(it.componentName, it.icon) } - val itemsByComponent = items.associateBy { it.componentName } - val itemsWithStructure = mutableListOf() - 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(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) { + 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 } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt index 9278570714fe2..17eb15b6a5565 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt @@ -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 @Mock(answer = Answers.RETURNS_DEEP_STUBS) private lateinit var cvh: ControlViewHolder + @Mock + private lateinit var metricsLogger: ControlsMetricsLogger companion object { fun any(): T = Mockito.any() @@ -86,7 +89,8 @@ class ControlActionCoordinatorImplTest : SysuiTestCase() { globalActionsComponent, taskViewFactory, getFakeBroadcastDispatcher(), - lazyUiController + lazyUiController, + metricsLogger )) `when`(cvh.cws.ci.controlId).thenReturn(ID)