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

Commit b08fc1d6 authored by Fabian Kozynski's avatar Fabian Kozynski
Browse files

Metrics for Tile Request Dialog

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

Change-Id: I6c9893809a24fd2a78e8cf3093da9537174a5949
parent cb9390f8
Loading
Loading
Loading
Loading
+116 −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.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
}
 No newline at end of file
+14 −2
Original line number Diff line number Diff line
@@ -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()
            )
        }
    }
}
 No newline at end of file
+41 −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

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)
    }
}
 No newline at end of file
+141 −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.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)
    }
}
 No newline at end of file
+90 −1
Original line number Diff line number Diff line
@@ -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())