diff options
5 files changed, 402 insertions, 3 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileRequestDialogEventLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/external/TileRequestDialogEventLogger.kt new file mode 100644 index 000000000000..e0f9cc28ca62 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileRequestDialogEventLogger.kt @@ -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 +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceRequestController.kt b/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceRequestController.kt index 210ee93bb7ef..73d6b971f785 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceRequestController.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceRequestController.kt @@ -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 diff --git a/packages/SystemUI/tests/src/com/android/systemui/InstanceIdSequenceFake.kt b/packages/SystemUI/tests/src/com/android/systemui/InstanceIdSequenceFake.kt new file mode 100644 index 000000000000..6fbe3ada2406 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/InstanceIdSequenceFake.kt @@ -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) + } +}
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileRequestDialogEventLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileRequestDialogEventLoggerTest.kt new file mode 100644 index 000000000000..64796f1a757a --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileRequestDialogEventLoggerTest.kt @@ -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) + } +}
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceRequestControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceRequestControllerTest.kt index 70e971cdbf32..a1c60a648de9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceRequestControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceRequestControllerTest.kt @@ -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 } @@ -102,6 +116,17 @@ class TileServiceRequestControllerTest : SysuiTestCase() { } @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()) verify(tileRequestDialog).setShowForAllUsers(true) @@ -114,6 +139,13 @@ class TileServiceRequestControllerTest : SysuiTestCase() { } @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 = ArgumentCaptor.forClass(DialogInterface.OnCancelListener::class.java) @@ -128,6 +160,25 @@ class TileServiceRequestControllerTest : SysuiTestCase() { } @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 = ArgumentCaptor.forClass(DialogInterface.OnClickListener::class.java) @@ -143,6 +194,25 @@ class TileServiceRequestControllerTest : SysuiTestCase() { } @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 = ArgumentCaptor.forClass(DialogInterface.OnClickListener::class.java) @@ -158,6 +228,25 @@ class TileServiceRequestControllerTest : SysuiTestCase() { } @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()) } |