Merge "Show highlight for device setting items" into main

This commit is contained in:
Haijie Hong
2024-09-13 08:58:44 +00:00
committed by Android (Google) Code Review
5 changed files with 163 additions and 55 deletions

View File

@@ -22,4 +22,7 @@ import kotlinx.coroutines.flow.Flow
data class DeviceSettingLayout(val rows: List<DeviceSettingLayoutRow>)
/** Represent a row in the layout. */
data class DeviceSettingLayoutRow(val settingIds: Flow<List<Int>>)
data class DeviceSettingLayoutRow(val columns: Flow<List<DeviceSettingLayoutColumn>>)
/** Represent a column in a row. */
data class DeviceSettingLayoutColumn(val settingId: Int, val highlighted: Boolean)

View File

@@ -20,12 +20,23 @@ import android.bluetooth.BluetoothAdapter
import android.content.Context
import android.media.AudioManager
import android.os.Bundle
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
@@ -43,7 +54,6 @@ import com.android.settings.core.SubSettingLauncher
import com.android.settings.overlay.FeatureFactory.Companion.featureFactory
import com.android.settings.spa.preference.ComposePreference
import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingIcon
import com.android.settingslib.spa.framework.theme.SettingsDimension
@@ -91,10 +101,16 @@ class DeviceDetailsFragmentFormatterImpl(
) : DeviceDetailsFragmentFormatter {
private val repository =
featureFactory.bluetoothFeatureProvider.getDeviceSettingRepository(
context, bluetoothAdapter, fragment.lifecycleScope)
context,
bluetoothAdapter,
fragment.lifecycleScope,
)
private val spatialAudioInteractor =
featureFactory.bluetoothFeatureProvider.getSpatialAudioInteractor(
context, context.getSystemService(AudioManager::class.java), fragment.lifecycleScope)
context,
context.getSystemService(AudioManager::class.java),
fragment.lifecycleScope,
)
private val viewModel: BluetoothDeviceDetailsViewModel =
ViewModelProvider(
fragment,
@@ -104,7 +120,8 @@ class DeviceDetailsFragmentFormatterImpl(
spatialAudioInteractor,
cachedDevice,
backgroundCoroutineContext,
))
),
)
.get(BluetoothDeviceDetailsViewModel::class.java)
override fun getVisiblePreferenceKeys(fragmentType: FragmentTypeModel): List<String>? =
@@ -120,7 +137,8 @@ class DeviceDetailsFragmentFormatterImpl(
viewModel
.getItems(fragmentType)
?.filterIsInstance<DeviceSettingConfigItemModel.BuiltinItem.BluetoothProfilesItem>()
?.first()?.invisibleProfiles
?.first()
?.invisibleProfiles
}
/** Updates bluetooth device details fragment layout. */
@@ -144,7 +162,8 @@ class DeviceDetailsFragmentFormatterImpl(
val settingId = items[row].settingId
if (settingIdToXmlPreferences.containsKey(settingId)) {
fragment.preferenceScreen.addPreference(
settingIdToXmlPreferences[settingId]!!.apply { order = row })
settingIdToXmlPreferences[settingId]!!.apply { order = row }
)
} else {
val pref =
ComposePreference(context)
@@ -169,7 +188,8 @@ class DeviceDetailsFragmentFormatterImpl(
emitAll(
viewModel.getDeviceSetting(cachedDevice, item.settingId).map {
it as? DeviceSettingPreferenceModel.HelpPreference
})
}
)
} ?: emit(null)
}
@@ -177,14 +197,15 @@ class DeviceDetailsFragmentFormatterImpl(
private fun buildPreference(layout: DeviceSettingLayout, row: Int) {
val contents by
remember(row) {
layout.rows[row].settingIds.flatMapLatest { settingIds ->
if (settingIds.isEmpty()) {
layout.rows[row].columns.flatMapLatest { columns ->
if (columns.isEmpty()) {
flowOf(emptyList<DeviceSettingPreferenceModel>())
} else {
combine(
settingIds.map { settingId ->
viewModel.getDeviceSetting(cachedDevice, settingId)
}) {
columns.map { column ->
viewModel.getDeviceSetting(cachedDevice, column.settingId)
}
) {
it.toList()
}
}
@@ -192,7 +213,40 @@ class DeviceDetailsFragmentFormatterImpl(
}
.collectAsStateWithLifecycle(initialValue = listOf())
val highlighted by
remember(row) {
layout.rows[row].columns.map { columns -> columns.any { it.highlighted } }
}
.collectAsStateWithLifecycle(initialValue = false)
val settings = contents
AnimatedVisibility(
visible = settings.isNotEmpty(),
enter = expandVertically(expandFrom = Alignment.Top),
exit = shrinkVertically(shrinkTowards = Alignment.Top),
) {
Box {
Box(
modifier =
Modifier.matchParentSize()
.padding(16.dp, 0.dp, 8.dp, 0.dp)
.background(
color =
if (highlighted) {
MaterialTheme.colorScheme.primaryContainer
} else {
Color.Transparent
},
shape = RoundedCornerShape(28.dp),
),
) {}
buildPreferences(settings)
}
}
}
@Composable
fun buildPreferences(settings: List<DeviceSettingPreferenceModel?>) {
when (settings.size) {
0 -> {}
1 -> {
@@ -217,11 +271,18 @@ class DeviceDetailsFragmentFormatterImpl(
}
}
else -> {
if (!settings.all { it is DeviceSettingPreferenceModel.MultiTogglePreference }) {
if (
!settings.all {
it is DeviceSettingPreferenceModel.MultiTogglePreference
}
) {
return
}
buildMultiTogglePreference(
settings.filterIsInstance<DeviceSettingPreferenceModel.MultiTogglePreference>())
settings.filterIsInstance<
DeviceSettingPreferenceModel.MultiTogglePreference
>()
)
}
}
}
@@ -243,11 +304,19 @@ class DeviceDetailsFragmentFormatterImpl(
override val onCheckedChange = { newChecked: Boolean ->
model.onCheckedChange(newChecked)
}
override val icon = @Composable { deviceSettingIcon(model.icon) }
override val icon: (@Composable () -> Unit)?
get() {
if (model.icon == null) {
return null
}
return { deviceSettingIcon(model.icon) }
}
}
if (model.onPrimaryClick != null) {
TwoTargetSwitchPreference(
switchPrefModel, primaryOnClick = model.onPrimaryClick::invoke)
switchPrefModel,
primaryOnClick = model.onPrimaryClick::invoke,
)
} else {
SwitchPreference(switchPrefModel)
}
@@ -263,8 +332,15 @@ class DeviceDetailsFragmentFormatterImpl(
model.onClick?.invoke()
Unit
}
override val icon = @Composable { deviceSettingIcon(model.icon) }
})
override val icon: (@Composable () -> Unit)?
get() {
if (model.icon == null) {
return null
}
return { deviceSettingIcon(model.icon) }
}
}
)
}
@Composable
@@ -281,11 +357,13 @@ class DeviceDetailsFragmentFormatterImpl(
.setDestination(DeviceDetailsMoreSettingsFragment::class.java.name)
.setSourceMetricsCategory(fragment.getMetricsCategory())
.setArguments(
Bundle().apply { putString(KEY_DEVICE_ADDRESS, cachedDevice.address) })
Bundle().apply { putString(KEY_DEVICE_ADDRESS, cachedDevice.address) }
)
.launch()
}
override val icon = @Composable { deviceSettingIcon(null) }
})
}
)
}
@Composable

View File

@@ -24,6 +24,7 @@ import androidx.lifecycle.viewModelScope
import com.android.settings.R
import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractor
import com.android.settings.bluetooth.ui.layout.DeviceSettingLayout
import com.android.settings.bluetooth.ui.layout.DeviceSettingLayoutColumn
import com.android.settings.bluetooth.ui.layout.DeviceSettingLayoutRow
import com.android.settings.bluetooth.ui.model.DeviceSettingPreferenceModel
import com.android.settings.bluetooth.ui.model.FragmentTypeModel
@@ -36,7 +37,6 @@ import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSetti
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
@@ -51,7 +51,7 @@ class BluetoothDeviceDetailsViewModel(
private val spatialAudioInteractor: SpatialAudioInteractor,
private val cachedDevice: CachedBluetoothDevice,
backgroundCoroutineContext: CoroutineContext,
) : AndroidViewModel(application){
) : AndroidViewModel(application) {
private val items =
viewModelScope.async(backgroundCoroutineContext, start = CoroutineStart.LAZY) {
@@ -74,7 +74,7 @@ class BluetoothDeviceDetailsViewModel(
fun getDeviceSetting(
cachedDevice: CachedBluetoothDevice,
@DeviceSettingId settingId: Int
@DeviceSettingId settingId: Int,
): Flow<DeviceSettingPreferenceModel?> {
if (settingId == DeviceSettingId.DEVICE_SETTING_ID_MORE_SETTINGS) {
return flowOf(DeviceSettingPreferenceModel.MoreSettingsPreference(settingId))
@@ -98,16 +98,19 @@ class BluetoothDeviceDetailsViewModel(
checked = switchState?.checked ?: false,
onCheckedChange = { newState ->
updateState?.invoke(
DeviceSettingStateModel.ActionSwitchPreferenceState(newState))
DeviceSettingStateModel.ActionSwitchPreferenceState(newState)
)
},
onPrimaryClick = { intent?.let { application.startActivity(it) } })
onPrimaryClick = { intent?.let { application.startActivity(it) } },
)
} else {
DeviceSettingPreferenceModel.PlainPreference(
id = id,
title = title,
summary = summary,
icon = icon,
onClick = { intent?.let { application.startActivity(it) } })
onClick = { intent?.let { application.startActivity(it) } },
)
}
}
is DeviceSettingModel.FooterPreference ->
@@ -116,9 +119,8 @@ class BluetoothDeviceDetailsViewModel(
DeviceSettingPreferenceModel.HelpPreference(
id = id,
icon = DeviceSettingIcon.ResourceIcon(R.drawable.ic_help),
onClick = {
application.startActivity(intent)
})
onClick = { application.startActivity(intent) },
)
is DeviceSettingModel.MultiTogglePreference ->
DeviceSettingPreferenceModel.MultiTogglePreference(
id = id,
@@ -129,7 +131,8 @@ class BluetoothDeviceDetailsViewModel(
isAllowedChangingState = isAllowedChangingState,
onSelectedChange = { newState ->
updateState(DeviceSettingStateModel.MultiTogglePreferenceState(newState))
})
},
)
is DeviceSettingModel.Unknown -> null
}
}
@@ -145,8 +148,8 @@ class BluetoothDeviceDetailsViewModel(
configItems.map { idToDeviceSetting[it.settingId] ?: flowOf(null) }
val positionToSettingIds =
combine(configDeviceSetting) { settings ->
val positionMapping = mutableMapOf<Int, List<Int>>()
var multiToggleSettingIds: MutableList<Int>? = null
val positionMapping = mutableMapOf<Int, List<DeviceSettingLayoutColumn>>()
var multiToggleSettingIds: MutableList<DeviceSettingLayoutColumn>? = null
for (i in settings.indices) {
val configItem = configItems[i]
val setting = settings[i]
@@ -156,14 +159,31 @@ class BluetoothDeviceDetailsViewModel(
}
if (setting !is DeviceSettingPreferenceModel.MultiTogglePreference) {
multiToggleSettingIds = null
positionMapping[i] = listOf(configItem.settingId)
positionMapping[i] =
listOf(
DeviceSettingLayoutColumn(
configItem.settingId,
configItem.highlighted,
)
)
continue
}
if (multiToggleSettingIds != null) {
multiToggleSettingIds.add(setting.id)
multiToggleSettingIds.add(
DeviceSettingLayoutColumn(
configItem.settingId,
configItem.highlighted,
)
)
} else {
multiToggleSettingIds = mutableListOf(setting.id)
multiToggleSettingIds =
mutableListOf(
DeviceSettingLayoutColumn(
configItem.settingId,
configItem.highlighted,
)
)
positionMapping[i] = multiToggleSettingIds
}
}
@@ -173,7 +193,8 @@ class BluetoothDeviceDetailsViewModel(
return DeviceSettingLayout(
configItems.indices.map { idx ->
DeviceSettingLayoutRow(positionToSettingIds.map { it[idx] ?: emptyList() })
})
}
)
}
class Factory(
@@ -186,9 +207,12 @@ class BluetoothDeviceDetailsViewModel(
override fun <T : ViewModel> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")
return BluetoothDeviceDetailsViewModel(
application, deviceSettingRepository, spatialAudioInteractor,
application,
deviceSettingRepository,
spatialAudioInteractor,
cachedDevice,
backgroundCoroutineContext)
backgroundCoroutineContext,
)
as T
}
}

View File

@@ -124,10 +124,11 @@ class DeviceDetailsFragmentFormatterTest {
listOf(
DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_HEADER,
"bluetooth_device_header"
highlighted = false,
preferenceKey = "bluetooth_device_header"
),
DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_ACTION_BUTTONS, "action_buttons"),
DeviceSettingId.DEVICE_SETTING_ID_ACTION_BUTTONS, highlighted = false, preferenceKey = "action_buttons"),
),
listOf(),
null))
@@ -157,7 +158,7 @@ class DeviceDetailsFragmentFormatterTest {
`when`(repository.getDeviceSettingsConfig(cachedDevice))
.thenReturn(
DeviceSettingConfigModel(
listOf(), listOf(), DeviceSettingConfigItemModel.AppProvidedItem(12345)))
listOf(), listOf(), DeviceSettingConfigItemModel.AppProvidedItem(12345, false)))
val intent = Intent().apply {
setAction(Intent.ACTION_VIEW)
setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
@@ -206,10 +207,10 @@ class DeviceDetailsFragmentFormatterTest {
listOf(
DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_HEADER,
"bluetooth_device_header"),
highlighted = false, preferenceKey = "bluetooth_device_header"),
DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_KEYBOARD_SETTINGS,
"keyboard_settings"),
highlighted = false, preferenceKey = "keyboard_settings"),
),
listOf(),
null))
@@ -230,12 +231,14 @@ class DeviceDetailsFragmentFormatterTest {
listOf(
DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_HEADER,
"bluetooth_device_header"),
highlighted = false,
preferenceKey = "bluetooth_device_header"),
DeviceSettingConfigItemModel.AppProvidedItem(
DeviceSettingId.DEVICE_SETTING_ID_ANC),
DeviceSettingId.DEVICE_SETTING_ID_ANC, highlighted = false),
DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_KEYBOARD_SETTINGS,
"keyboard_settings"),
highlighted = false,
preferenceKey = "keyboard_settings"),
),
listOf(),
null))

View File

@@ -246,11 +246,11 @@ class BluetoothDeviceDetailsViewModelTest {
}
private fun getLatestLayout(layout: DeviceSettingLayout): List<List<Int>> {
var latestLayout = MutableList(layout.rows.size) { emptyList<Int>() }
val latestLayout = MutableList(layout.rows.size) { emptyList<Int>() }
for (i in layout.rows.indices) {
layout.rows[i]
.settingIds
.onEach { latestLayout[i] = it }
.columns
.onEach { latestLayout[i] = it.map { c -> c.settingId } }
.launchIn(testScope.backgroundScope)
}
@@ -278,15 +278,15 @@ class BluetoothDeviceDetailsViewModelTest {
DeviceSettingModel.ActionSwitchPreference(cachedDevice, settingId, "title")
private fun buildRemoteSettingItem(settingId: Int) =
DeviceSettingConfigItemModel.AppProvidedItem(settingId)
DeviceSettingConfigItemModel.AppProvidedItem(settingId, false)
private companion object {
val BUILTIN_SETTING_ITEM_1 =
DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_HEADER, "bluetooth_device_header")
DeviceSettingId.DEVICE_SETTING_ID_HEADER, false, "bluetooth_device_header")
val BUILDIN_SETTING_ITEM_2 =
DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_ACTION_BUTTONS, "action_buttons")
val SETTING_ITEM_HELP = DeviceSettingConfigItemModel.AppProvidedItem(12345)
DeviceSettingId.DEVICE_SETTING_ID_ACTION_BUTTONS, false, "action_buttons")
val SETTING_ITEM_HELP = DeviceSettingConfigItemModel.AppProvidedItem(12345, false)
}
}