Merge changes Iadb68373,I6c989380

* changes:
  Use Theme.SystemUI.Dialog for the TileRequest dialog
  Metrics for Tile Request Dialog
This commit is contained in:
Fabian Kozynski
2021-11-30 19:43:14 +00:00
committed by Android (Google) Code Review
11 changed files with 407 additions and 42 deletions

View File

@@ -30,7 +30,6 @@
android:layout_marginBottom="16dp"
android:textDirection="locale"
android:textAlignment="viewStart"
android:textAppearance="@style/TextAppearance.PrivacyDialog"
android:lineHeight="20sp"
android:textAppearance="@style/TextAppearance.Dialog.Body"
/>
</LinearLayout>

View File

@@ -660,16 +660,6 @@
<item name="android:windowAnimationStyle">@android:style/Animation.Dialog</item>
</style>
<!-- TileService request dialog -->
<style name="TileRequestDialog" parent="Theme.SystemUI.QuickSettings.Dialog">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@drawable/qs_dialog_bg</item>
<item name="android:windowIsFloating">true</item>
<item name="android:backgroundDimEnabled">true</item>
<item name="android:windowCloseOnTouchOutside">true</item>
<item name="android:windowAnimationStyle">@android:style/Animation.Dialog</item>
</style>
<!-- USB Contaminant dialog -->
<style name ="USBContaminant" />

View File

@@ -18,11 +18,8 @@ package com.android.systemui.qs.external
import android.content.Context
import android.graphics.drawable.Icon
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.view.WindowInsets
import android.widget.TextView
import com.android.systemui.R
import com.android.systemui.plugins.qs.QSTile
@@ -38,25 +35,12 @@ import com.android.systemui.statusbar.phone.SystemUIDialog
*/
class TileRequestDialog(
context: Context
) : SystemUIDialog(context, R.style.TileRequestDialog) {
) : SystemUIDialog(context) {
companion object {
internal val CONTENT_ID = R.id.content
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window?.apply {
attributes.fitInsetsTypes = attributes.fitInsetsTypes or WindowInsets.Type.statusBars()
attributes.receiveInsetsIgnoringZOrder = true
setLayout(
context.resources
.getDimensionPixelSize(R.dimen.qs_tile_service_request_dialog_width),
WRAP_CONTENT
)
}
}
/**
* Set the data of the tile to add, to show the user.
*/
@@ -76,9 +60,7 @@ class TileRequestDialog(
context.resources.getDimensionPixelSize(R.dimen.qs_quick_tile_size)
)
}
val spacing = context.resources.getDimensionPixelSize(
R.dimen.qs_tile_service_request_content_space
)
val spacing = 0
setView(ll, spacing, spacing, spacing, spacing / 2)
}

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

@@ -253,7 +253,7 @@ public class QSIconViewImpl extends QSIconView {
return Utils.getColorAttrDefaultColor(context, android.R.attr.textColorPrimary);
case Tile.STATE_ACTIVE:
return Utils.getColorAttrDefaultColor(context,
android.R.attr.textColorPrimaryInverse);
com.android.internal.R.attr.textColorOnAccent);
default:
Log.e("QSIconView", "Invalid state " + state);
return 0;

View File

@@ -88,7 +88,7 @@ open class QSTileViewImpl @JvmOverloads constructor(
private val colorUnavailable = Utils.applyAlpha(UNAVAILABLE_ALPHA, colorInactive)
private val colorLabelActive =
Utils.getColorAttrDefaultColor(context, android.R.attr.textColorPrimaryInverse)
Utils.getColorAttrDefaultColor(context, com.android.internal.R.attr.textColorOnAccent)
private val colorLabelInactive =
Utils.getColorAttrDefaultColor(context, android.R.attr.textColorPrimary)
private val colorLabelUnavailable = Utils.applyAlpha(UNAVAILABLE_ALPHA, colorLabelInactive)

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

@@ -59,11 +59,6 @@ class TileRequestDialogTest : SysuiTestCase() {
}
}
@Test
fun useCorrectTheme() {
assertThat(dialog.context.themeResId).isEqualTo(R.style.TileRequestDialog)
}
@Test
fun setTileData_hasCorrectViews() {
val icon = Icon.createWithResource(mContext, R.drawable.cloud)

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())