Metrics for Tile Request Dialog

Test: atest TileRequestDialogEventLoggerImplTest
Test: atest TileServiceRequestControllerTest
Test: manual: statsd logging
Fixes: 207482791

Change-Id: I6c9893809a24fd2a78e8cf3093da9537174a5949
This commit is contained in:
Fabian Kozynski
2021-11-23 12:22:23 -05:00
parent cb9390f872
commit b08fc1d6e4
5 changed files with 402 additions and 3 deletions

View File

@@ -0,0 +1,116 @@
/*
* 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.qs.external
import android.app.StatusBarManager
import androidx.annotation.VisibleForTesting
import com.android.internal.logging.InstanceId
import com.android.internal.logging.InstanceIdSequence
import com.android.internal.logging.UiEvent
import com.android.internal.logging.UiEventLogger
import com.android.internal.logging.UiEventLoggerImpl
class TileRequestDialogEventLogger @VisibleForTesting constructor(
private val uiEventLogger: UiEventLogger,
private val instanceIdSequence: InstanceIdSequence
) {
companion object {
const val MAX_INSTANCE_ID = 1 shl 20
}
constructor() : this(UiEventLoggerImpl(), InstanceIdSequence(MAX_INSTANCE_ID))
/**
* Obtain a new [InstanceId] to log a session for a dialog request.
*/
fun newInstanceId(): InstanceId = instanceIdSequence.newInstanceId()
/**
* Log that the dialog has been shown to the user for a tile in the given [packageName]. This
* call should use a new [instanceId].
*/
fun logDialogShown(packageName: String, instanceId: InstanceId) {
uiEventLogger.logWithInstanceId(
TileRequestDialogEvent.TILE_REQUEST_DIALOG_SHOWN,
/* uid */ 0,
packageName,
instanceId
)
}
/**
* Log the user response to the dialog being shown. Must follow a call to [logDialogShown] that
* used the same [packageName] and [instanceId]. Only the following responses are valid:
* * [StatusBarManager.TILE_ADD_REQUEST_RESULT_DIALOG_DISMISSED]
* * [StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_NOT_ADDED]
* * [StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ADDED]
*/
fun logUserResponse(
@StatusBarManager.RequestResult response: Int,
packageName: String,
instanceId: InstanceId
) {
val event = when (response) {
StatusBarManager.TILE_ADD_REQUEST_RESULT_DIALOG_DISMISSED -> {
TileRequestDialogEvent.TILE_REQUEST_DIALOG_DISMISSED
}
StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_NOT_ADDED -> {
TileRequestDialogEvent.TILE_REQUEST_DIALOG_TILE_NOT_ADDED
}
StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ADDED -> {
TileRequestDialogEvent.TILE_REQUEST_DIALOG_TILE_ADDED
}
else -> {
throw IllegalArgumentException("User response not valid: $response")
}
}
uiEventLogger.logWithInstanceId(event, /* uid */ 0, packageName, instanceId)
}
/**
* Log that the dialog will not be shown because the tile was already part of the active set.
* Corresponds to a response of [StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ALREADY_ADDED].
*/
fun logTileAlreadyAdded(packageName: String, instanceId: InstanceId) {
uiEventLogger.logWithInstanceId(
TileRequestDialogEvent.TILE_REQUEST_DIALOG_TILE_ALREADY_ADDED,
/* uid */ 0,
packageName,
instanceId
)
}
}
enum class TileRequestDialogEvent(private val _id: Int) : UiEventLogger.UiEventEnum {
@UiEvent(doc = "Tile request dialog not shown because tile is already added.")
TILE_REQUEST_DIALOG_TILE_ALREADY_ADDED(917),
@UiEvent(doc = "Tile request dialog shown to user.")
TILE_REQUEST_DIALOG_SHOWN(918),
@UiEvent(doc = "User dismisses dialog without choosing an option.")
TILE_REQUEST_DIALOG_DISMISSED(919),
@UiEvent(doc = "User accepts adding tile from dialog.")
TILE_REQUEST_DIALOG_TILE_ADDED(920),
@UiEvent(doc = "User denies adding tile from dialog.")
TILE_REQUEST_DIALOG_TILE_NOT_ADDED(921);
override fun getId() = _id
}

View File

@@ -44,6 +44,7 @@ class TileServiceRequestController constructor(
private val qsTileHost: QSTileHost,
private val commandQueue: CommandQueue,
private val commandRegistry: CommandRegistry,
private val eventLogger: TileRequestDialogEventLogger,
private val dialogCreator: () -> TileRequestDialog = { TileRequestDialog(qsTileHost.context) }
) {
@@ -97,25 +98,31 @@ class TileServiceRequestController constructor(
icon: Icon?,
callback: Consumer<Int>
) {
val instanceId = eventLogger.newInstanceId()
val packageName = componentName.packageName
if (isTileAlreadyAdded(componentName)) {
callback.accept(TILE_ALREADY_ADDED)
eventLogger.logTileAlreadyAdded(packageName, instanceId)
return
}
val dialogResponse = Consumer<Int> { response ->
if (response == ADD_TILE) {
addTile(componentName)
}
dialogCanceller = null
eventLogger.logUserResponse(response, packageName, instanceId)
callback.accept(response)
}
val tileData = TileRequestDialog.TileData(appName, label, icon)
createDialog(tileData, dialogResponse).also { dialog ->
dialogCanceller = {
if (componentName.packageName == it) {
if (packageName == it) {
dialog.cancel()
}
dialogCanceller = null
}
}.show()
eventLogger.logDialogShown(packageName, instanceId)
}
private fun createDialog(
@@ -168,7 +175,12 @@ class TileServiceRequestController constructor(
private val commandRegistry: CommandRegistry
) {
fun create(qsTileHost: QSTileHost): TileServiceRequestController {
return TileServiceRequestController(qsTileHost, commandQueue, commandRegistry)
return TileServiceRequestController(
qsTileHost,
commandQueue,
commandRegistry,
TileRequestDialogEventLogger()
)
}
}
}

View File

@@ -0,0 +1,41 @@
/*
* 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
import com.android.internal.logging.InstanceId
import com.android.internal.logging.InstanceIdSequence
/**
* Fake [InstanceId] generator.
*/
class InstanceIdSequenceFake(instanceIdMax: Int) : InstanceIdSequence(instanceIdMax) {
/**
* Last id used to generate a [InstanceId]. `-1` if no [InstanceId] has been generated.
*/
var lastInstanceId = -1
private set
override fun newInstanceId(): InstanceId {
if (lastInstanceId == -1 || lastInstanceId == mInstanceIdMax - 1) {
lastInstanceId = 1
} else {
lastInstanceId++
}
return newInstanceIdInternal(lastInstanceId)
}
}

View File

@@ -0,0 +1,141 @@
/*
* 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.qs.external
import android.app.StatusBarManager
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import androidx.test.filters.SmallTest
import com.android.internal.logging.InstanceId
import com.android.internal.logging.UiEventLogger
import com.android.internal.logging.testing.UiEventLoggerFake
import com.android.systemui.InstanceIdSequenceFake
import com.android.systemui.SysuiTestCase
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@SmallTest
@RunWith(AndroidTestingRunner::class)
@TestableLooper.RunWithLooper
class TileRequestDialogEventLoggerTest : SysuiTestCase() {
companion object {
private const val PACKAGE_NAME = "package"
}
private lateinit var uiEventLogger: UiEventLoggerFake
private val instanceIdSequence =
InstanceIdSequenceFake(TileRequestDialogEventLogger.MAX_INSTANCE_ID)
private lateinit var logger: TileRequestDialogEventLogger
@Before
fun setUp() {
uiEventLogger = UiEventLoggerFake()
logger = TileRequestDialogEventLogger(uiEventLogger, instanceIdSequence)
}
@Test
fun testInstanceIdsFromSequence() {
(1..10).forEach {
assertThat(logger.newInstanceId().id).isEqualTo(instanceIdSequence.lastInstanceId)
}
}
@Test
fun testLogTileAlreadyAdded() {
val instanceId = instanceIdSequence.newInstanceId()
logger.logTileAlreadyAdded(PACKAGE_NAME, instanceId)
assertThat(uiEventLogger.numLogs()).isEqualTo(1)
uiEventLogger[0].match(
TileRequestDialogEvent.TILE_REQUEST_DIALOG_TILE_ALREADY_ADDED,
instanceId
)
}
@Test
fun testLogDialogShown() {
val instanceId = instanceIdSequence.newInstanceId()
logger.logDialogShown(PACKAGE_NAME, instanceId)
assertThat(uiEventLogger.numLogs()).isEqualTo(1)
uiEventLogger[0].match(TileRequestDialogEvent.TILE_REQUEST_DIALOG_SHOWN, instanceId)
}
@Test
fun testLogDialogDismissed() {
val instanceId = instanceIdSequence.newInstanceId()
logger.logUserResponse(
StatusBarManager.TILE_ADD_REQUEST_RESULT_DIALOG_DISMISSED,
PACKAGE_NAME,
instanceId
)
assertThat(uiEventLogger.numLogs()).isEqualTo(1)
uiEventLogger[0].match(TileRequestDialogEvent.TILE_REQUEST_DIALOG_DISMISSED, instanceId)
}
@Test
fun testLogDialogTileNotAdded() {
val instanceId = instanceIdSequence.newInstanceId()
logger.logUserResponse(
StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_NOT_ADDED,
PACKAGE_NAME,
instanceId
)
assertThat(uiEventLogger.numLogs()).isEqualTo(1)
uiEventLogger[0]
.match(TileRequestDialogEvent.TILE_REQUEST_DIALOG_TILE_NOT_ADDED, instanceId)
}
@Test
fun testLogDialogTileAdded() {
val instanceId = instanceIdSequence.newInstanceId()
logger.logUserResponse(
StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ADDED,
PACKAGE_NAME,
instanceId
)
assertThat(uiEventLogger.numLogs()).isEqualTo(1)
uiEventLogger[0].match(TileRequestDialogEvent.TILE_REQUEST_DIALOG_TILE_ADDED, instanceId)
}
@Test(expected = IllegalArgumentException::class)
fun testLogResponseInvalid_throws() {
val instanceId = instanceIdSequence.newInstanceId()
logger.logUserResponse(
-1,
PACKAGE_NAME,
instanceId
)
}
private fun UiEventLoggerFake.FakeUiEvent.match(
event: UiEventLogger.UiEventEnum,
instanceId: InstanceId
) {
assertThat(eventId).isEqualTo(event.id)
assertThat(uid).isEqualTo(0)
assertThat(packageName).isEqualTo(PACKAGE_NAME)
assertThat(this.instanceId).isEqualTo(instanceId)
}
}

View File

@@ -16,18 +16,22 @@
package com.android.systemui.qs.external
import android.app.StatusBarManager
import android.content.ComponentName
import android.content.DialogInterface
import android.graphics.drawable.Icon
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.internal.logging.InstanceId
import com.android.internal.statusbar.IAddTileResultCallback
import com.android.systemui.InstanceIdSequenceFake
import com.android.systemui.SysuiTestCase
import com.android.systemui.qs.QSTileHost
import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.commandline.CommandRegistry
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.eq
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
@@ -63,18 +67,28 @@ class TileServiceRequestControllerTest : SysuiTestCase() {
@Mock
private lateinit var commandQueue: CommandQueue
@Mock
private lateinit var logger: TileRequestDialogEventLogger
@Mock
private lateinit var icon: Icon
private val instanceIdSequence = InstanceIdSequenceFake(1_000)
private lateinit var controller: TileServiceRequestController
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
`when`(logger.newInstanceId()).thenReturn(instanceIdSequence.newInstanceId())
// Tile not present by default
`when`(qsTileHost.indexOf(anyString())).thenReturn(-1)
controller = TileServiceRequestController(qsTileHost, commandQueue, commandRegistry) {
controller = TileServiceRequestController(
qsTileHost,
commandQueue,
commandRegistry,
logger
) {
tileRequestDialog
}
@@ -101,6 +115,17 @@ class TileServiceRequestControllerTest : SysuiTestCase() {
verify(qsTileHost, never()).addTile(any(ComponentName::class.java), anyBoolean())
}
@Test
fun tileAlreadyAdded_logged() {
`when`(qsTileHost.indexOf(CustomTile.toSpec(TEST_COMPONENT))).thenReturn(2)
controller.requestTileAdd(TEST_COMPONENT, TEST_APP_NAME, TEST_LABEL, icon) {}
verify(logger).logTileAlreadyAdded(eq<String>(TEST_COMPONENT.packageName), any())
verify(logger, never()).logDialogShown(anyString(), any())
verify(logger, never()).logUserResponse(anyInt(), anyString(), any())
}
@Test
fun showAllUsers_set() {
controller.requestTileAdd(TEST_COMPONENT, TEST_APP_NAME, TEST_LABEL, icon, Callback())
@@ -113,6 +138,13 @@ class TileServiceRequestControllerTest : SysuiTestCase() {
verify(tileRequestDialog).setCanceledOnTouchOutside(true)
}
@Test
fun dialogShown_logged() {
controller.requestTileAdd(TEST_COMPONENT, TEST_APP_NAME, TEST_LABEL, icon) {}
verify(logger).logDialogShown(eq<String>(TEST_COMPONENT.packageName), any())
}
@Test
fun cancelListener_dismissResult() {
val cancelListenerCaptor =
@@ -127,6 +159,25 @@ class TileServiceRequestControllerTest : SysuiTestCase() {
verify(qsTileHost, never()).addTile(any(ComponentName::class.java), anyBoolean())
}
@Test
fun dialogCancelled_logged() {
val cancelListenerCaptor =
ArgumentCaptor.forClass(DialogInterface.OnCancelListener::class.java)
controller.requestTileAdd(TEST_COMPONENT, TEST_APP_NAME, TEST_LABEL, icon) {}
val instanceId = InstanceId.fakeInstanceId(instanceIdSequence.lastInstanceId)
verify(tileRequestDialog).setOnCancelListener(capture(cancelListenerCaptor))
verify(logger).logDialogShown(TEST_COMPONENT.packageName, instanceId)
cancelListenerCaptor.value.onCancel(tileRequestDialog)
verify(logger).logUserResponse(
StatusBarManager.TILE_ADD_REQUEST_RESULT_DIALOG_DISMISSED,
TEST_COMPONENT.packageName,
instanceId
)
}
@Test
fun positiveActionListener_tileAddedResult() {
val clickListenerCaptor =
@@ -142,6 +193,25 @@ class TileServiceRequestControllerTest : SysuiTestCase() {
verify(qsTileHost).addTile(TEST_COMPONENT, /* end */ true)
}
@Test
fun tileAdded_logged() {
val clickListenerCaptor =
ArgumentCaptor.forClass(DialogInterface.OnClickListener::class.java)
controller.requestTileAdd(TEST_COMPONENT, TEST_APP_NAME, TEST_LABEL, icon) {}
val instanceId = InstanceId.fakeInstanceId(instanceIdSequence.lastInstanceId)
verify(tileRequestDialog).setPositiveButton(anyInt(), capture(clickListenerCaptor))
verify(logger).logDialogShown(TEST_COMPONENT.packageName, instanceId)
clickListenerCaptor.value.onClick(tileRequestDialog, DialogInterface.BUTTON_POSITIVE)
verify(logger).logUserResponse(
StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ADDED,
TEST_COMPONENT.packageName,
instanceId
)
}
@Test
fun negativeActionListener_tileNotAddedResult() {
val clickListenerCaptor =
@@ -157,6 +227,25 @@ class TileServiceRequestControllerTest : SysuiTestCase() {
verify(qsTileHost, never()).addTile(any(ComponentName::class.java), anyBoolean())
}
@Test
fun tileNotAdded_logged() {
val clickListenerCaptor =
ArgumentCaptor.forClass(DialogInterface.OnClickListener::class.java)
controller.requestTileAdd(TEST_COMPONENT, TEST_APP_NAME, TEST_LABEL, icon) {}
val instanceId = InstanceId.fakeInstanceId(instanceIdSequence.lastInstanceId)
verify(tileRequestDialog).setNegativeButton(anyInt(), capture(clickListenerCaptor))
verify(logger).logDialogShown(TEST_COMPONENT.packageName, instanceId)
clickListenerCaptor.value.onClick(tileRequestDialog, DialogInterface.BUTTON_NEGATIVE)
verify(logger).logUserResponse(
StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_NOT_ADDED,
TEST_COMPONENT.packageName,
instanceId
)
}
@Test
fun commandQueueCallback_registered() {
verify(commandQueue).addCallback(any())