diff --git a/src/com/android/settings/bluetooth/ui/layout/DeviceSettingLayout.kt b/src/com/android/settings/bluetooth/ui/layout/DeviceSettingLayout.kt index 87e2e8b4962..5987e5a2079 100644 --- a/src/com/android/settings/bluetooth/ui/layout/DeviceSettingLayout.kt +++ b/src/com/android/settings/bluetooth/ui/layout/DeviceSettingLayout.kt @@ -22,4 +22,7 @@ import kotlinx.coroutines.flow.Flow data class DeviceSettingLayout(val rows: List) /** Represent a row in the layout. */ -data class DeviceSettingLayoutRow(val settingIds: Flow>) +data class DeviceSettingLayoutRow(val columns: Flow>) + +/** Represent a column in a row. */ +data class DeviceSettingLayoutColumn(val settingId: Int, val highlighted: Boolean) diff --git a/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt b/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt index f2a569d2245..a5997e7bc83 100644 --- a/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt +++ b/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt @@ -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? = @@ -120,7 +137,8 @@ class DeviceDetailsFragmentFormatterImpl( viewModel .getItems(fragmentType) ?.filterIsInstance() - ?.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,22 +197,56 @@ 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()) } else { combine( - settingIds.map { settingId -> - viewModel.getDeviceSetting(cachedDevice, settingId) - }) { - it.toList() + columns.map { column -> + viewModel.getDeviceSetting(cachedDevice, column.settingId) } + ) { + it.toList() + } } } } .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) { 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()) + 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 diff --git a/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModel.kt b/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModel.kt index 1071adce37f..67a0ebc8398 100644 --- a/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModel.kt +++ b/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModel.kt @@ -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 { 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>() - var multiToggleSettingIds: MutableList? = null + val positionMapping = mutableMapOf>() + var multiToggleSettingIds: MutableList? = 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 create(modelClass: Class): T { @Suppress("UNCHECKED_CAST") return BluetoothDeviceDetailsViewModel( - application, deviceSettingRepository, spatialAudioInteractor, + application, + deviceSettingRepository, + spatialAudioInteractor, cachedDevice, - backgroundCoroutineContext) + backgroundCoroutineContext, + ) as T } } diff --git a/tests/robotests/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatterTest.kt b/tests/robotests/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatterTest.kt index 8070b2e5362..51c0c3076ee 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatterTest.kt +++ b/tests/robotests/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatterTest.kt @@ -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)) diff --git a/tests/robotests/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModelTest.kt b/tests/robotests/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModelTest.kt index 6869c23fa95..c3f938c3c46 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModelTest.kt +++ b/tests/robotests/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModelTest.kt @@ -246,11 +246,11 @@ class BluetoothDeviceDetailsViewModelTest { } private fun getLatestLayout(layout: DeviceSettingLayout): List> { - var latestLayout = MutableList(layout.rows.size) { emptyList() } + val latestLayout = MutableList(layout.rows.size) { emptyList() } 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) } }