Add metrics for new bluetooth device details

BUG: 343317785
Test: atest DeviceDetailsFragmentFormatterTest
Flag: com.android.settings.flags.enable_bluetooth_device_details_polish
Change-Id: Ic74a885627a1426c338b093dcf949688fe9784d1
This commit is contained in:
Haijie Hong
2024-11-14 15:31:23 +08:00
parent aaa040e085
commit fb9d83ad68
2 changed files with 144 additions and 39 deletions

View File

@@ -17,6 +17,7 @@
package com.android.settings.bluetooth.ui.view package com.android.settings.bluetooth.ui.view
import android.app.ActivityOptions import android.app.ActivityOptions
import android.app.settings.SettingsEnums
import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothAdapter
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@@ -39,6 +40,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference import androidx.preference.Preference
import com.android.settings.R import com.android.settings.R
import com.android.settings.SettingsPreferenceFragment import com.android.settings.SettingsPreferenceFragment
@@ -50,30 +52,33 @@ import com.android.settings.bluetooth.ui.model.FragmentTypeModel
import com.android.settings.bluetooth.ui.view.DeviceDetailsMoreSettingsFragment.Companion.KEY_DEVICE_ADDRESS import com.android.settings.bluetooth.ui.view.DeviceDetailsMoreSettingsFragment.Companion.KEY_DEVICE_ADDRESS
import com.android.settings.bluetooth.ui.viewmodel.BluetoothDeviceDetailsViewModel import com.android.settings.bluetooth.ui.viewmodel.BluetoothDeviceDetailsViewModel
import com.android.settings.core.SubSettingLauncher import com.android.settings.core.SubSettingLauncher
import com.android.settings.overlay.FeatureFactory
import com.android.settings.spa.preference.ComposePreference import com.android.settings.spa.preference.ComposePreference
import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingActionModel import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingActionModel
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingIcon import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingIcon
import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.framework.theme.SettingsDimension
import com.android.settingslib.spa.widget.button.ActionButton
import com.android.settingslib.spa.widget.button.ActionButtons
import com.android.settingslib.spa.widget.preference.Preference as SpaPreference import com.android.settingslib.spa.widget.preference.Preference as SpaPreference
import com.android.settingslib.spa.widget.preference.PreferenceModel import com.android.settingslib.spa.widget.preference.PreferenceModel
import com.android.settingslib.spa.widget.preference.SwitchPreference import com.android.settingslib.spa.widget.preference.SwitchPreference
import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
import com.android.settingslib.spa.widget.preference.TwoTargetSwitchPreference import com.android.settingslib.spa.widget.preference.TwoTargetSwitchPreference
import com.android.settingslib.spa.widget.scaffold.RegularScaffold
import com.android.settingslib.spa.widget.ui.Footer import com.android.settingslib.spa.widget.ui.Footer
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
/** Handles device details fragment layout according to config. */ /** Handles device details fragment layout according to config. */
@@ -93,6 +98,7 @@ interface DeviceDetailsFragmentFormatter {
): Flow<DeviceSettingPreferenceModel.HelpPreference?> ): Flow<DeviceSettingPreferenceModel.HelpPreference?>
} }
@FlowPreview
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class DeviceDetailsFragmentFormatterImpl( class DeviceDetailsFragmentFormatterImpl(
private val context: Context, private val context: Context,
@@ -101,6 +107,9 @@ class DeviceDetailsFragmentFormatterImpl(
private val cachedDevice: CachedBluetoothDevice, private val cachedDevice: CachedBluetoothDevice,
private val backgroundCoroutineContext: CoroutineContext, private val backgroundCoroutineContext: CoroutineContext,
) : DeviceDetailsFragmentFormatter { ) : DeviceDetailsFragmentFormatter {
private val metricsFeatureProvider = FeatureFactory.featureFactory.metricsFeatureProvider
private val prefVisibility = mutableMapOf<String, MutableStateFlow<Boolean>>()
private val prefVisibilityJobs = mutableListOf<Job>()
private val viewModel: BluetoothDeviceDetailsViewModel = private val viewModel: BluetoothDeviceDetailsViewModel =
ViewModelProvider( ViewModelProvider(
@@ -147,21 +156,33 @@ class DeviceDetailsFragmentFormatterImpl(
prefKeyToSettingId[pref.key]?.let { id -> settingIdToXmlPreferences[id] = pref } prefKeyToSettingId[pref.key]?.let { id -> settingIdToXmlPreferences[id] = pref }
} }
fragment.preferenceScreen.removeAll() fragment.preferenceScreen.removeAll()
for (job in prefVisibilityJobs) {
job.cancel()
}
prefVisibilityJobs.clear()
for (row in items.indices) { for (row in items.indices) {
val settingId = items[row].settingId val settingId = items[row].settingId
if (settingIdToXmlPreferences.containsKey(settingId)) { if (settingIdToXmlPreferences.containsKey(settingId)) {
fragment.preferenceScreen.addPreference( fragment.preferenceScreen.addPreference(
settingIdToXmlPreferences[settingId]!!.apply { order = row } settingIdToXmlPreferences[settingId]!!
.apply { order = row }
.also { logItemShown(it.key, it.isVisible) }
) )
} else { } else {
val prefKey = getPreferenceKey(settingId)
prefVisibilityJobs.add(
getDevicesSettingForRow(layout, row)
.onEach { logItemShown(prefKey, it.isNotEmpty()) }
.launchIn(fragment.lifecycleScope)
)
val pref = val pref =
ComposePreference(context) ComposePreference(context)
.apply { .apply {
key = getPreferenceKey(settingId) key = prefKey
order = row order = row
} }
.also { pref -> pref.setContent { buildPreference(layout, row) } } .also { pref -> pref.setContent { buildPreference(layout, row, prefKey) } }
fragment.preferenceScreen.addPreference(pref) fragment.preferenceScreen.addPreference(pref)
} }
} }
@@ -183,24 +204,28 @@ class DeviceDetailsFragmentFormatterImpl(
} ?: emit(null) } ?: emit(null)
} }
@Composable private fun getDevicesSettingForRow(
private fun buildPreference(layout: DeviceSettingLayout, row: Int) { layout: DeviceSettingLayout,
val contents by row: Int,
remember(row) { ): Flow<List<DeviceSettingPreferenceModel>> =
layout.rows[row].columns.flatMapLatest { columns -> layout.rows[row].columns.flatMapLatest { columns ->
if (columns.isEmpty()) { if (columns.isEmpty()) {
flowOf(emptyList<DeviceSettingPreferenceModel>()) flowOf(emptyList())
} else { } else {
combine( combine(
columns.map { column -> columns.map { column ->
viewModel.getDeviceSetting(cachedDevice, column.settingId) viewModel.getDeviceSetting(cachedDevice, column.settingId)
}
) {
it.toList()
}
}
} }
) {
it.toList().filterNotNull()
} }
}
}
@Composable
private fun buildPreference(layout: DeviceSettingLayout, row: Int, prefKey: String) {
val contents by
remember(row) { getDevicesSettingForRow(layout, row) }
.collectAsStateWithLifecycle(initialValue = listOf()) .collectAsStateWithLifecycle(initialValue = listOf())
val highlighted by val highlighted by
@@ -226,31 +251,31 @@ class DeviceDetailsFragmentFormatterImpl(
shape = RoundedCornerShape(28.dp), shape = RoundedCornerShape(28.dp),
) )
) {} ) {}
buildPreferences(settings) buildPreferences(settings, prefKey)
} }
} }
} }
@Composable @Composable
fun buildPreferences(settings: List<DeviceSettingPreferenceModel?>) { fun buildPreferences(settings: List<DeviceSettingPreferenceModel?>, prefKey: String) {
when (settings.size) { when (settings.size) {
0 -> {} 0 -> {}
1 -> { 1 -> {
when (val setting = settings[0]) { when (val setting = settings[0]) {
is DeviceSettingPreferenceModel.PlainPreference -> { is DeviceSettingPreferenceModel.PlainPreference -> {
buildPlainPreference(setting) buildPlainPreference(setting, prefKey)
} }
is DeviceSettingPreferenceModel.SwitchPreference -> { is DeviceSettingPreferenceModel.SwitchPreference -> {
buildSwitchPreference(setting) buildSwitchPreference(setting, prefKey)
} }
is DeviceSettingPreferenceModel.MultiTogglePreference -> { is DeviceSettingPreferenceModel.MultiTogglePreference -> {
buildMultiTogglePreference(setting) buildMultiTogglePreference(setting, prefKey)
} }
is DeviceSettingPreferenceModel.FooterPreference -> { is DeviceSettingPreferenceModel.FooterPreference -> {
buildFooterPreference(setting) buildFooterPreference(setting)
} }
is DeviceSettingPreferenceModel.MoreSettingsPreference -> { is DeviceSettingPreferenceModel.MoreSettingsPreference -> {
buildMoreSettingsPreference() buildMoreSettingsPreference(prefKey)
} }
is DeviceSettingPreferenceModel.HelpPreference -> {} is DeviceSettingPreferenceModel.HelpPreference -> {}
null -> {} null -> {}
@@ -262,20 +287,32 @@ class DeviceDetailsFragmentFormatterImpl(
@Composable @Composable
private fun buildMultiTogglePreference( private fun buildMultiTogglePreference(
pref: DeviceSettingPreferenceModel.MultiTogglePreference pref: DeviceSettingPreferenceModel.MultiTogglePreference,
prefKey: String,
) { ) {
MultiTogglePreference(pref) MultiTogglePreference(
pref.copy(
onSelectedChange = { newState ->
logItemClick(prefKey, newState)
pref.onSelectedChange(newState)
}
)
)
} }
@Composable @Composable
private fun buildSwitchPreference(model: DeviceSettingPreferenceModel.SwitchPreference) { private fun buildSwitchPreference(
model: DeviceSettingPreferenceModel.SwitchPreference,
prefKey: String,
) {
val switchPrefModel = val switchPrefModel =
object : SwitchPreferenceModel { object : SwitchPreferenceModel {
override val title = model.title override val title = model.title
override val summary = { model.summary ?: "" } override val summary = { model.summary ?: "" }
override val checked = { model.checked } override val checked = { model.checked }
override val onCheckedChange = { newChecked: Boolean -> override val onCheckedChange = { newState: Boolean ->
model.onCheckedChange(newChecked) logItemClick(prefKey, if (newState) EVENT_SWITCH_ON else EVENT_SWITCH_OFF)
model.onCheckedChange(newState)
} }
override val changeable = { !model.disabled } override val changeable = { !model.disabled }
override val icon: (@Composable () -> Unit)? override val icon: (@Composable () -> Unit)?
@@ -289,8 +326,11 @@ class DeviceDetailsFragmentFormatterImpl(
if (model.action != null) { if (model.action != null) {
TwoTargetSwitchPreference( TwoTargetSwitchPreference(
switchPrefModel, switchPrefModel,
primaryOnClick = { triggerAction(model.action) }, primaryOnClick = {
primaryEnabled = { !model.disabled } logItemClick(prefKey, EVENT_CLICK_PRIMARY)
triggerAction(model.action)
},
primaryEnabled = { !model.disabled },
) )
} else { } else {
SwitchPreference(switchPrefModel) SwitchPreference(switchPrefModel)
@@ -298,12 +338,16 @@ class DeviceDetailsFragmentFormatterImpl(
} }
@Composable @Composable
private fun buildPlainPreference(model: DeviceSettingPreferenceModel.PlainPreference) { private fun buildPlainPreference(
model: DeviceSettingPreferenceModel.PlainPreference,
prefKey: String,
) {
SpaPreference( SpaPreference(
object : PreferenceModel { object : PreferenceModel {
override val title = model.title override val title = model.title
override val summary = { model.summary ?: "" } override val summary = { model.summary ?: "" }
override val onClick = { override val onClick = {
logItemClick(prefKey, EVENT_CLICK_PRIMARY)
model.action?.let { triggerAction(it) } model.action?.let { triggerAction(it) }
Unit Unit
} }
@@ -319,7 +363,7 @@ class DeviceDetailsFragmentFormatterImpl(
} }
@Composable @Composable
fun buildMoreSettingsPreference() { fun buildMoreSettingsPreference(prefKey: String) {
SpaPreference( SpaPreference(
object : PreferenceModel { object : PreferenceModel {
override val title = override val title =
@@ -328,6 +372,7 @@ class DeviceDetailsFragmentFormatterImpl(
context.getString(R.string.bluetooth_device_more_settings_preference_summary) context.getString(R.string.bluetooth_device_more_settings_preference_summary)
} }
override val onClick = { override val onClick = {
logItemClick(prefKey, EVENT_CLICK_PRIMARY)
SubSettingLauncher(context) SubSettingLauncher(context)
.setDestination(DeviceDetailsMoreSettingsFragment::class.java.name) .setDestination(DeviceDetailsMoreSettingsFragment::class.java.name)
.setSourceMetricsCategory(fragment.getMetricsCategory()) .setSourceMetricsCategory(fragment.getMetricsCategory())
@@ -356,6 +401,35 @@ class DeviceDetailsFragmentFormatterImpl(
icon?.let { Icon(it, modifier = Modifier.size(SettingsDimension.itemIconSize)) } icon?.let { Icon(it, modifier = Modifier.size(SettingsDimension.itemIconSize)) }
} }
private fun logItemClick(preferenceKey: String, value: Int = 0) {
logAction(preferenceKey, SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_ITEM_CLICKED, value)
}
private fun logItemShown(preferenceKey: String, visible: Boolean) {
if (!visible && !prefVisibility.containsKey(preferenceKey)) {
return
}
prefVisibility
.computeIfAbsent(preferenceKey) {
MutableStateFlow(true).also { visibilityFlow ->
visibilityFlow
.onEach {
logAction(
preferenceKey,
SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_ITEM_SHOWN,
if (it) EVENT_VISIBLE else EVENT_INVISIBLE,
)
}
.launchIn(fragment.lifecycleScope)
}
}
.value = visible
}
private fun logAction(preferenceKey: String, action: Int, value: Int) {
metricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN, action, 0, preferenceKey, value)
}
private fun triggerAction(action: DeviceSettingActionModel) { private fun triggerAction(action: DeviceSettingActionModel) {
when (action) { when (action) {
is DeviceSettingActionModel.IntentAction -> { is DeviceSettingActionModel.IntentAction -> {
@@ -375,7 +449,12 @@ class DeviceDetailsFragmentFormatterImpl(
private fun getPreferenceKey(settingId: Int) = "DEVICE_SETTING_${settingId}" private fun getPreferenceKey(settingId: Int) = "DEVICE_SETTING_${settingId}"
companion object { private companion object {
const val TAG = "DeviceDetailsFormatter" const val TAG = "DeviceDetailsFormatter"
const val EVENT_SWITCH_OFF = 0
const val EVENT_SWITCH_ON = 1
const val EVENT_CLICK_PRIMARY = 2
const val EVENT_INVISIBLE = 0
const val EVENT_VISIBLE = 1
} }
} }

View File

@@ -16,6 +16,7 @@
package com.android.settings.bluetooth.ui.view package com.android.settings.bluetooth.ui.view
import android.app.settings.SettingsEnums;
import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothAdapter
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@@ -39,6 +40,7 @@ import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSetti
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel
import com.android.settingslib.bluetooth.devicesettings.shared.model.ToggleModel import com.android.settingslib.bluetooth.devicesettings.shared.model.ToggleModel
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@@ -53,6 +55,7 @@ import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.eq import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito.any import org.mockito.Mockito.any
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` import org.mockito.Mockito.`when`
import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule import org.mockito.junit.MockitoRule
@@ -62,6 +65,7 @@ import org.robolectric.shadows.ShadowLooper
import org.robolectric.shadows.ShadowLooper.shadowMainLooper import org.robolectric.shadows.ShadowLooper.shadowMainLooper
@ExperimentalCoroutinesApi
@RunWith(RobolectricTestRunner::class) @RunWith(RobolectricTestRunner::class)
class DeviceDetailsFragmentFormatterTest { class DeviceDetailsFragmentFormatterTest {
@get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule() @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
@@ -70,6 +74,7 @@ class DeviceDetailsFragmentFormatterTest {
@Mock private lateinit var bluetoothAdapter: BluetoothAdapter @Mock private lateinit var bluetoothAdapter: BluetoothAdapter
@Mock private lateinit var repository: DeviceSettingRepository @Mock private lateinit var repository: DeviceSettingRepository
private lateinit var context: Context
private lateinit var fragment: TestFragment private lateinit var fragment: TestFragment
private lateinit var underTest: DeviceDetailsFragmentFormatter private lateinit var underTest: DeviceDetailsFragmentFormatter
private lateinit var featureFactory: FakeFeatureFactory private lateinit var featureFactory: FakeFeatureFactory
@@ -78,7 +83,7 @@ class DeviceDetailsFragmentFormatterTest {
@Before @Before
fun setUp() { fun setUp() {
val context = ApplicationProvider.getApplicationContext<Context>() context = ApplicationProvider.getApplicationContext()
featureFactory = FakeFeatureFactory.setupForTest() featureFactory = FakeFeatureFactory.setupForTest()
`when`( `when`(
featureFactory.bluetoothFeatureProvider.getDeviceSettingRepository( featureFactory.bluetoothFeatureProvider.getDeviceSettingRepository(
@@ -204,9 +209,22 @@ class DeviceDetailsFragmentFormatterTest {
null)) null))
underTest.updateLayout(FragmentTypeModel.DeviceDetailsMainFragment) underTest.updateLayout(FragmentTypeModel.DeviceDetailsMainFragment)
runCurrent()
assertThat(getDisplayedPreferences().mapNotNull { it.key }) assertThat(getDisplayedPreferences().mapNotNull { it.key })
.containsExactly("bluetooth_device_header", "keyboard_settings") .containsExactly("bluetooth_device_header", "keyboard_settings")
verify(featureFactory.metricsFeatureProvider)
.action(
SettingsEnums.PAGE_UNKNOWN,
SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_ITEM_SHOWN,
0,
"bluetooth_device_header", 1)
verify(featureFactory.metricsFeatureProvider)
.action(
SettingsEnums.PAGE_UNKNOWN,
SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_ITEM_SHOWN,
0,
"keyboard_settings", 1)
} }
} }
@@ -249,12 +267,20 @@ class DeviceDetailsFragmentFormatterTest {
updateState = {}))) updateState = {})))
underTest.updateLayout(FragmentTypeModel.DeviceDetailsMainFragment) underTest.updateLayout(FragmentTypeModel.DeviceDetailsMainFragment)
runCurrent()
assertThat(getDisplayedPreferences().mapNotNull { it.key }) assertThat(getDisplayedPreferences().mapNotNull { it.key })
.containsExactly( .containsExactly(
"bluetooth_device_header", "bluetooth_device_header",
"DEVICE_SETTING_${DeviceSettingId.DEVICE_SETTING_ID_ANC}", "DEVICE_SETTING_${DeviceSettingId.DEVICE_SETTING_ID_ANC}",
"keyboard_settings") "keyboard_settings")
verify(featureFactory.metricsFeatureProvider)
.action(
SettingsEnums.PAGE_UNKNOWN,
SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_ITEM_SHOWN,
0,
"DEVICE_SETTING_${DeviceSettingId.DEVICE_SETTING_ID_ANC}", 1
)
} }
} }