diff --git a/res/values/strings.xml b/res/values/strings.xml index 939befe4b74..c405e126cfc 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1864,6 +1864,10 @@ Device details Keyboard settings + + More settings + + Firmware updates, about, and more Device\'s Bluetooth address: %1$s @@ -1884,6 +1888,9 @@ Disconnect app + + More settings + Maximum connected Bluetooth audio devices diff --git a/res/xml/bluetooth_device_more_settings_fragment.xml b/res/xml/bluetooth_device_more_settings_fragment.xml new file mode 100644 index 00000000000..4fb4acae03c --- /dev/null +++ b/res/xml/bluetooth_device_more_settings_fragment.xml @@ -0,0 +1,24 @@ + + + + + + + diff --git a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java index bd762a1ef11..54250f59f29 100644 --- a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java +++ b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java @@ -48,6 +48,7 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.settings.R; +import com.android.settings.bluetooth.ui.model.FragmentTypeModel; import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatter; import com.android.settings.connecteddevice.stylus.StylusDevicesController; import com.android.settings.core.SettingsUIDeviceConfig; @@ -343,7 +344,7 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment public void onCreatePreferences(@NonNull Bundle savedInstanceState, @NonNull String rootKey) { super.onCreatePreferences(savedInstanceState, rootKey); if (Flags.enableBluetoothDeviceDetailsPolish()) { - mFormatter.updateLayout(); + mFormatter.updateLayout(FragmentTypeModel.DeviceDetailsMainFragment.INSTANCE); } } @@ -400,7 +401,9 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment @Override protected void addPreferenceController(AbstractPreferenceController controller) { if (Flags.enableBluetoothDeviceDetailsPolish()) { - List keys = mFormatter.getVisiblePreferenceKeysForMainPage(); + List keys = + mFormatter.getVisiblePreferenceKeys( + FragmentTypeModel.DeviceDetailsMainFragment.INSTANCE); Lifecycle lifecycle = getSettingsLifecycle(); if (keys == null || keys.contains(controller.getPreferenceKey())) { super.addPreferenceController(controller); diff --git a/src/com/android/settings/bluetooth/ui/composable/MultiTogglePreferenceGroup.kt b/src/com/android/settings/bluetooth/ui/composable/MultiTogglePreferenceGroup.kt index 8fe3c255d34..d29795efee7 100644 --- a/src/com/android/settings/bluetooth/ui/composable/MultiTogglePreferenceGroup.kt +++ b/src/com/android/settings/bluetooth/ui/composable/MultiTogglePreferenceGroup.kt @@ -66,15 +66,14 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.DialogProperties import com.android.settings.R +import com.android.settings.bluetooth.ui.model.DeviceSettingPreferenceModel import com.android.settings.bluetooth.ui.composable.Icon as DeviceSettingComposeIcon -import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel -import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.widget.dialog.getDialogWidth @Composable fun MultiTogglePreferenceGroup( - preferenceModels: List, + preferenceModels: List, ) { var settingIdForPopUp by remember { mutableStateOf(null) } @@ -115,7 +114,7 @@ fun MultiTogglePreferenceGroup( colors = getButtonColors(preferenceModel.isActive), contentPadding = PaddingValues(0.dp)) { DeviceSettingComposeIcon( - preferenceModel.toggles[preferenceModel.state.selectedIndex] + preferenceModel.toggles[preferenceModel.selectedIndex] .icon, modifier = Modifier.size(24.dp)) } @@ -144,7 +143,7 @@ private fun getButtonColors(isActive: Boolean) = @OptIn(ExperimentalMaterial3Api::class) @Composable private fun dialog( - multiTogglePreference: DeviceSettingModel.MultiTogglePreference, + multiTogglePreference: DeviceSettingPreferenceModel.MultiTogglePreference, onDismiss: () -> Unit ) { BasicAlertDialog( @@ -179,7 +178,7 @@ private fun dialog( } @Composable -private fun dialogContent(multiTogglePreference: DeviceSettingModel.MultiTogglePreference) { +private fun dialogContent(multiTogglePreference: DeviceSettingPreferenceModel.MultiTogglePreference) { Column { Row( modifier = Modifier.fillMaxWidth().height(24.dp), @@ -219,7 +218,7 @@ private fun dialogContent(multiTogglePreference: DeviceSettingModel.MultiToggleP } Row { for ((idx, toggle) in multiTogglePreference.toggles.withIndex()) { - val selected = idx == multiTogglePreference.state.selectedIndex + val selected = idx == multiTogglePreference.selectedIndex Column( modifier = Modifier.weight(1f) @@ -237,8 +236,7 @@ private fun dialogContent(multiTogglePreference: DeviceSettingModel.MultiToggleP ) { Button( onClick = { - multiTogglePreference.updateState( - DeviceSettingStateModel.MultiTogglePreferenceState(idx)) + multiTogglePreference.onSelectedChange(idx) }, modifier = Modifier.fillMaxSize(), colors = diff --git a/src/com/android/settings/bluetooth/ui/model/DeviceSettingPreferenceModel.kt b/src/com/android/settings/bluetooth/ui/model/DeviceSettingPreferenceModel.kt new file mode 100644 index 00000000000..6612591fce4 --- /dev/null +++ b/src/com/android/settings/bluetooth/ui/model/DeviceSettingPreferenceModel.kt @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2024 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.settings.bluetooth.ui.model + +import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId +import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingIcon +import com.android.settingslib.bluetooth.devicesettings.shared.model.ToggleModel + +/** Models a device setting preference. */ +sealed interface DeviceSettingPreferenceModel { + @DeviceSettingId + val id: Int + + /** Models a plain preference. */ + data class PlainPreference( + @DeviceSettingId override val id: Int, + val title: String, + val summary: String? = null, + val icon: DeviceSettingIcon? = null, + val onClick: (() -> Unit)? = null, + ) : DeviceSettingPreferenceModel + + /** Models a switch preference. */ + data class SwitchPreference( + @DeviceSettingId override val id: Int, + val title: String, + val summary: String? = null, + val icon: DeviceSettingIcon? = null, + val checked: Boolean, + val onCheckedChange: ((Boolean) -> Unit), + val onPrimaryClick: (() -> Unit)? = null, + ) : DeviceSettingPreferenceModel + + /** Models a multi-toggle preference. */ + data class MultiTogglePreference( + @DeviceSettingId override val id: Int, + val title: String, + val toggles: List, + val isActive: Boolean, + val selectedIndex: Int, + val isAllowedChangingState: Boolean, + val onSelectedChange: (Int) -> Unit, + ) : DeviceSettingPreferenceModel + + /** Models a footer preference. */ + data class FooterPreference( + @DeviceSettingId override val id: Int, + val footerText: String, + ) : DeviceSettingPreferenceModel + + /** Models a preference which could navigate to more settings fragment. */ + data class MoreSettingsPreference( + @DeviceSettingId override val id: Int, + ) : DeviceSettingPreferenceModel +} diff --git a/src/com/android/settings/bluetooth/ui/model/FragmentTypeModel.kt b/src/com/android/settings/bluetooth/ui/model/FragmentTypeModel.kt new file mode 100644 index 00000000000..19858c4ba7d --- /dev/null +++ b/src/com/android/settings/bluetooth/ui/model/FragmentTypeModel.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 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.settings.bluetooth.ui.model + +/** Models a device details fragment type. */ +sealed interface FragmentTypeModel { + /** Device details main page. */ + data object DeviceDetailsMainFragment : FragmentTypeModel + /** Device details more settings page. */ + data object DeviceDetailsMoreSettingsFragment : FragmentTypeModel +} diff --git a/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt b/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt index b75579dfa0d..c933c754b7e 100644 --- a/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt +++ b/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt @@ -19,47 +19,52 @@ package com.android.settings.bluetooth.ui.view import android.bluetooth.BluetoothAdapter import android.content.Context import android.media.AudioManager -import android.util.Log +import android.os.Bundle import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.preference.Preference +import com.android.settings.R import com.android.settings.SettingsPreferenceFragment import com.android.settings.bluetooth.ui.composable.Icon import com.android.settings.bluetooth.ui.composable.MultiTogglePreferenceGroup import com.android.settings.bluetooth.ui.layout.DeviceSettingLayout +import com.android.settings.bluetooth.ui.model.DeviceSettingPreferenceModel +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.viewmodel.BluetoothDeviceDetailsViewModel +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.shared.model.DeviceSettingConfigItemModel -import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel -import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel +import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingIcon import com.android.settingslib.spa.framework.theme.SettingsDimension +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.SwitchPreference import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel import com.android.settingslib.spa.widget.preference.TwoTargetSwitchPreference +import com.android.settingslib.spa.widget.ui.Footer import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking -import com.android.settingslib.spa.widget.preference.Preference as SpaPreference - /** Handles device details fragment layout according to config. */ interface DeviceDetailsFragmentFormatter { /** Gets keys of visible preferences in built-in preference in xml. */ - fun getVisiblePreferenceKeysForMainPage(): List? + fun getVisiblePreferenceKeys(fragmentType: FragmentTypeModel): List? /** Updates device details fragment layout. */ - fun updateLayout() + fun updateLayout(fragmentType: FragmentTypeModel) } @OptIn(ExperimentalCoroutinesApi::class) @@ -79,23 +84,25 @@ class DeviceDetailsFragmentFormatterImpl( ViewModelProvider( fragment, BluetoothDeviceDetailsViewModel.Factory( + fragment.requireActivity().application, repository, spatialAudioInteractor, cachedDevice, )) .get(BluetoothDeviceDetailsViewModel::class.java) - override fun getVisiblePreferenceKeysForMainPage(): List? = runBlocking { - viewModel - .getItems() - ?.filterIsInstance() - ?.mapNotNull { it.preferenceKey } - } + override fun getVisiblePreferenceKeys(fragmentType: FragmentTypeModel): List? = + runBlocking { + viewModel + .getItems(fragmentType) + ?.filterIsInstance() + ?.mapNotNull { it.preferenceKey } + } /** Updates bluetooth device details fragment layout. */ - override fun updateLayout() = runBlocking { - val items = viewModel.getItems() ?: return@runBlocking - val layout = viewModel.getLayout() ?: return@runBlocking + override fun updateLayout(fragmentType: FragmentTypeModel) = runBlocking { + val items = viewModel.getItems(fragmentType) ?: return@runBlocking + val layout = viewModel.getLayout(fragmentType) ?: return@runBlocking val prefKeyToSettingId = items .filterIsInstance() @@ -124,6 +131,8 @@ class DeviceDetailsFragmentFormatterImpl( fragment.preferenceScreen.addPreference(pref) } } + // TODO(b/343317785): figure out how to remove the foot preference. + fragment.preferenceScreen.addPreference(Preference(context).apply { order = 10000 }) } @Composable @@ -132,7 +141,7 @@ class DeviceDetailsFragmentFormatterImpl( remember(row) { layout.rows[row].settingIds.flatMapLatest { settingIds -> if (settingIds.isEmpty()) { - flowOf(emptyList()) + flowOf(emptyList()) } else { combine( settingIds.map { settingId -> @@ -150,72 +159,104 @@ class DeviceDetailsFragmentFormatterImpl( 0 -> {} 1 -> { when (val setting = settings[0]) { - is DeviceSettingModel.ActionSwitchPreference -> { - buildActionSwitchPreference(setting) + is DeviceSettingPreferenceModel.PlainPreference -> { + buildPlainPreference(setting) } - is DeviceSettingModel.MultiTogglePreference -> { + is DeviceSettingPreferenceModel.SwitchPreference -> { + buildSwitchPreference(setting) + } + is DeviceSettingPreferenceModel.MultiTogglePreference -> { buildMultiTogglePreference(listOf(setting)) } - null -> {} - else -> { - Log.w(TAG, "Unknown preference type ${setting.id}, skip.") + is DeviceSettingPreferenceModel.FooterPreference -> { + buildFooterPreference(setting) } + is DeviceSettingPreferenceModel.MoreSettingsPreference -> { + buildMoreSettingsPreference() + } + null -> {} } } else -> { - if (!settings.all { it is DeviceSettingModel.MultiTogglePreference }) { + if (!settings.all { it is DeviceSettingPreferenceModel.MultiTogglePreference }) { return } buildMultiTogglePreference( - settings.filterIsInstance()) + settings.filterIsInstance()) } } } @Composable - private fun buildMultiTogglePreference(prefs: List) { + private fun buildMultiTogglePreference( + prefs: List + ) { MultiTogglePreferenceGroup(prefs) } @Composable - private fun buildActionSwitchPreference(model: DeviceSettingModel.ActionSwitchPreference) { - if (model.switchState != null) { - val switchPrefModel = - object : SwitchPreferenceModel { - override val title = model.title - override val summary = { model.summary ?: "" } - override val checked = { model.switchState?.checked } - override val onCheckedChange = { newChecked: Boolean -> - model.updateState?.invoke( - DeviceSettingStateModel.ActionSwitchPreferenceState(newChecked)) - Unit - } - override val icon = @Composable { deviceSettingIcon(model) } + private fun buildSwitchPreference(model: DeviceSettingPreferenceModel.SwitchPreference) { + val switchPrefModel = + object : SwitchPreferenceModel { + override val title = model.title + override val summary = { model.summary ?: "" } + override val checked = { model.checked } + override val onCheckedChange = { newChecked: Boolean -> + model.onCheckedChange(newChecked) } - if (model.intent != null) { - TwoTargetSwitchPreference(switchPrefModel) { context.startActivity(model.intent) } - } else { - SwitchPreference(switchPrefModel) + override val icon = @Composable { deviceSettingIcon(model.icon) } } + if (model.onPrimaryClick != null) { + TwoTargetSwitchPreference( + switchPrefModel, primaryOnClick = model.onPrimaryClick::invoke) } else { - SpaPreference( - object : PreferenceModel { - override val title = model.title - override val summary = { model.summary ?: "" } - override val onClick = { - model.intent?.let { context.startActivity(it) } - Unit - } - override val icon = @Composable { deviceSettingIcon(model) } - }) + SwitchPreference(switchPrefModel) } } @Composable - private fun deviceSettingIcon(model: DeviceSettingModel.ActionSwitchPreference) { - model.icon?.let { icon -> - Icon(icon, modifier = Modifier.size(SettingsDimension.itemIconSize)) - } + private fun buildPlainPreference(model: DeviceSettingPreferenceModel.PlainPreference) { + SpaPreference( + object : PreferenceModel { + override val title = model.title + override val summary = { model.summary ?: "" } + override val onClick = { + model.onClick?.invoke() + Unit + } + override val icon = @Composable { deviceSettingIcon(model.icon) } + }) + } + + @Composable + fun buildMoreSettingsPreference() { + SpaPreference( + object : PreferenceModel { + override val title = + stringResource(R.string.bluetooth_device_more_settings_preference_title) + override val summary = { + context.getString(R.string.bluetooth_device_more_settings_preference_summary) + } + override val onClick = { + SubSettingLauncher(context) + .setDestination(DeviceDetailsMoreSettingsFragment::class.java.name) + .setSourceMetricsCategory(fragment.getMetricsCategory()) + .setArguments( + Bundle().apply { putString(KEY_DEVICE_ADDRESS, cachedDevice.address) }) + .launch() + } + override val icon = @Composable { deviceSettingIcon(null) } + }) + } + + @Composable + fun buildFooterPreference(model: DeviceSettingPreferenceModel.FooterPreference) { + Footer(footerText = model.footerText) + } + + @Composable + private fun deviceSettingIcon(icon: DeviceSettingIcon?) { + icon?.let { Icon(it, modifier = Modifier.size(SettingsDimension.itemIconSize)) } } private fun getPreferenceKey(settingId: Int) = "DEVICE_SETTING_${settingId}" diff --git a/src/com/android/settings/bluetooth/ui/view/DeviceDetailsMoreSettingsFragment.kt b/src/com/android/settings/bluetooth/ui/view/DeviceDetailsMoreSettingsFragment.kt new file mode 100644 index 00000000000..c648a3e9b43 --- /dev/null +++ b/src/com/android/settings/bluetooth/ui/view/DeviceDetailsMoreSettingsFragment.kt @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2024 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.settings.bluetooth.ui.view + +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothManager +import android.content.Context +import android.os.Bundle +import com.android.settings.R +import com.android.settings.bluetooth.BluetoothDetailsProfilesController +import com.android.settings.bluetooth.Utils +import com.android.settings.bluetooth.ui.model.FragmentTypeModel +import com.android.settings.dashboard.DashboardFragment +import com.android.settings.overlay.FeatureFactory.Companion.featureFactory +import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.settingslib.bluetooth.LocalBluetoothManager +import com.android.settingslib.core.AbstractPreferenceController +import com.android.settingslib.core.lifecycle.LifecycleObserver + +class DeviceDetailsMoreSettingsFragment : DashboardFragment() { + private lateinit var formatter: DeviceDetailsFragmentFormatter + private lateinit var localBluetoothManager: LocalBluetoothManager + private lateinit var cachedDevice: CachedBluetoothDevice + + // TODO(b/343317785): add metrics category + override fun getMetricsCategory(): Int = 0 + + override fun getPreferenceScreenResId(): Int { + return R.xml.bluetooth_device_more_settings_fragment + } + + override fun addPreferenceController(controller: AbstractPreferenceController) { + val keys: List? = + formatter.getVisiblePreferenceKeys(FragmentTypeModel.DeviceDetailsMoreSettingsFragment) + val lifecycle = settingsLifecycle + if (keys == null || keys.contains(controller.preferenceKey)) { + super.addPreferenceController(controller) + } else if (controller is LifecycleObserver) { + lifecycle.removeObserver((controller as LifecycleObserver)) + } + } + + private fun getCachedDevice(): CachedBluetoothDevice? { + val bluetoothAddress = arguments?.getString(KEY_DEVICE_ADDRESS) ?: return null + localBluetoothManager = Utils.getLocalBtManager(context) ?: return null + val remoteDevice: BluetoothDevice = + localBluetoothManager.bluetoothAdapter.getRemoteDevice(bluetoothAddress) ?: return null + return Utils.getLocalBtManager(context).cachedDeviceManager.findDevice(remoteDevice) + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + super.onCreatePreferences(savedInstanceState, rootKey) + formatter.updateLayout(FragmentTypeModel.DeviceDetailsMoreSettingsFragment) + } + + override fun createPreferenceControllers(context: Context): List { + val bluetoothManager = context.getSystemService(BluetoothManager::class.java) + cachedDevice = + getCachedDevice() + ?: run { + finish() + return emptyList() + } + formatter = + featureFactory.bluetoothFeatureProvider.getDeviceDetailsFragmentFormatter( + requireContext(), this, bluetoothManager.adapter, cachedDevice) + return listOf( + BluetoothDetailsProfilesController( + context, this, localBluetoothManager, cachedDevice, settingsLifecycle)) + } + + override fun getLogTag(): String = TAG + + companion object { + const val TAG: String = "DeviceMoreSettingsFrg" + const val KEY_DEVICE_ADDRESS: String = "device_address" + } +} diff --git a/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModel.kt b/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModel.kt index befff830da3..c85015cc71b 100644 --- a/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModel.kt +++ b/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModel.kt @@ -16,17 +16,22 @@ package com.android.settings.bluetooth.ui.viewmodel +import android.app.Application +import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractor import com.android.settings.bluetooth.ui.layout.DeviceSettingLayout import com.android.settings.bluetooth.ui.layout.DeviceSettingLayoutRow +import com.android.settings.bluetooth.ui.model.DeviceSettingPreferenceModel +import com.android.settings.bluetooth.ui.model.FragmentTypeModel import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel +import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async @@ -38,30 +43,81 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn class BluetoothDeviceDetailsViewModel( + private val application: Application, private val deviceSettingRepository: DeviceSettingRepository, private val spatialAudioInteractor: SpatialAudioInteractor, private val cachedDevice: CachedBluetoothDevice, -) : ViewModel() { +) : AndroidViewModel(application){ + private val items = viewModelScope.async(Dispatchers.IO, start = CoroutineStart.LAZY) { deviceSettingRepository.getDeviceSettingsConfig(cachedDevice) } - suspend fun getItems(): List? = items.await()?.mainItems + suspend fun getItems(fragment: FragmentTypeModel): List? = + when (fragment) { + is FragmentTypeModel.DeviceDetailsMainFragment -> items.await()?.mainItems + is FragmentTypeModel.DeviceDetailsMoreSettingsFragment -> + items.await()?.moreSettingsItems + } fun getDeviceSetting( cachedDevice: CachedBluetoothDevice, @DeviceSettingId settingId: Int - ): Flow { + ): Flow { + if (settingId == DeviceSettingId.DEVICE_SETTING_ID_MORE_SETTINGS) { + return flowOf(DeviceSettingPreferenceModel.MoreSettingsPreference(settingId)) + } return when (settingId) { DeviceSettingId.DEVICE_SETTING_ID_SPATIAL_AUDIO_MULTI_TOGGLE -> spatialAudioInteractor.getDeviceSetting(cachedDevice) else -> deviceSettingRepository.getDeviceSetting(cachedDevice, settingId) + }.map { it?.toPreferenceModel() } + } + + private fun DeviceSettingModel.toPreferenceModel(): DeviceSettingPreferenceModel? { + return when (this) { + is DeviceSettingModel.ActionSwitchPreference -> { + if (switchState != null) { + DeviceSettingPreferenceModel.SwitchPreference( + id = id, + title = title, + summary = summary, + icon = icon, + checked = switchState?.checked ?: false, + onCheckedChange = { newState -> + updateState?.invoke( + DeviceSettingStateModel.ActionSwitchPreferenceState(newState)) + }, + onPrimaryClick = { intent?.let { application.startActivity(it) } }) + } else { + DeviceSettingPreferenceModel.PlainPreference( + id = id, + title = title, + summary = summary, + icon = icon, + onClick = { intent?.let { application.startActivity(it) } }) + } + } + is DeviceSettingModel.FooterPreference -> + DeviceSettingPreferenceModel.FooterPreference(id = id, footerText = footerText) + is DeviceSettingModel.MultiTogglePreference -> + DeviceSettingPreferenceModel.MultiTogglePreference( + id = id, + title = title, + toggles = toggles, + isActive = isActive, + selectedIndex = state.selectedIndex, + isAllowedChangingState = isAllowedChangingState, + onSelectedChange = { newState -> + updateState(DeviceSettingStateModel.MultiTogglePreferenceState(newState)) + }) + is DeviceSettingModel.Unknown -> null } } - suspend fun getLayout(): DeviceSettingLayout? { - val configItems = getItems() ?: return null + suspend fun getLayout(fragment: FragmentTypeModel): DeviceSettingLayout? { + val configItems = getItems(fragment) ?: return null val idToDeviceSetting = configItems .filterIsInstance() @@ -80,7 +136,7 @@ class BluetoothDeviceDetailsViewModel( if (!isXmlPreference && setting == null) { continue } - if (setting !is DeviceSettingModel.MultiTogglePreference) { + if (setting !is DeviceSettingPreferenceModel.MultiTogglePreference) { multiToggleSettingIds = null positionMapping[i] = listOf(configItem.settingId) continue @@ -103,6 +159,7 @@ class BluetoothDeviceDetailsViewModel( } class Factory( + private val application: Application, private val deviceSettingRepository: DeviceSettingRepository, private val spatialAudioInteractor: SpatialAudioInteractor, private val cachedDevice: CachedBluetoothDevice, @@ -110,7 +167,7 @@ class BluetoothDeviceDetailsViewModel( override fun create(modelClass: Class): T { @Suppress("UNCHECKED_CAST") return BluetoothDeviceDetailsViewModel( - deviceSettingRepository, spatialAudioInteractor, cachedDevice) + application, deviceSettingRepository, spatialAudioInteractor, cachedDevice) as T } } diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragmentTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragmentTest.java index 19d0eddd3a4..c84d42c1618 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragmentTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragmentTest.java @@ -50,6 +50,7 @@ import androidx.fragment.app.FragmentTransaction; import androidx.preference.PreferenceScreen; import com.android.settings.R; +import com.android.settings.bluetooth.ui.model.FragmentTypeModel; import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatter; import com.android.settings.testutils.FakeFeatureFactory; import com.android.settingslib.bluetooth.CachedBluetoothDevice; @@ -117,7 +118,9 @@ public class BluetoothDeviceDetailsFragmentTest { FakeFeatureFactory fakeFeatureFactory = FakeFeatureFactory.setupForTest(); when(fakeFeatureFactory.mBluetoothFeatureProvider.getDeviceDetailsFragmentFormatter(any(), any(), any(), eq(mCachedDevice))).thenReturn(mFormatter); - when(mFormatter.getVisiblePreferenceKeysForMainPage()).thenReturn(null); + when(mFormatter.getVisiblePreferenceKeys( + FragmentTypeModel.DeviceDetailsMainFragment.INSTANCE)) + .thenReturn(null); mFragment = setupFragment(); mFragment.onAttach(mContext); 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 609d7679f16..251b814f972 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 @@ -26,6 +26,7 @@ import androidx.preference.PreferenceManager import androidx.preference.PreferenceScreen import androidx.test.core.app.ApplicationProvider import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractor +import com.android.settings.bluetooth.ui.model.FragmentTypeModel import com.android.settings.dashboard.DashboardFragment import com.android.settings.testutils.FakeFeatureFactory import com.android.settingslib.bluetooth.CachedBluetoothDevice @@ -45,7 +46,6 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.eq import org.mockito.Mock import org.mockito.Mockito.any @@ -111,10 +111,9 @@ class DeviceDetailsFragmentFormatterTest { DeviceSettingConfigItemModel.BuiltinItem( DeviceSettingId.DEVICE_SETTING_ID_ACTION_BUTTONS, "action_buttons"), ), - listOf(), - "footer")) + listOf())) - val keys = underTest.getVisiblePreferenceKeysForMainPage() + val keys = underTest.getVisiblePreferenceKeys(FragmentTypeModel.DeviceDetailsMainFragment) assertThat(keys).containsExactly("bluetooth_device_header", "action_buttons") } @@ -125,7 +124,7 @@ class DeviceDetailsFragmentFormatterTest { testScope.runTest { `when`(repository.getDeviceSettingsConfig(cachedDevice)).thenReturn(null) - val keys = underTest.getVisiblePreferenceKeysForMainPage() + val keys = underTest.getVisiblePreferenceKeys(FragmentTypeModel.DeviceDetailsMainFragment) assertThat(keys).isNull() } @@ -136,9 +135,9 @@ class DeviceDetailsFragmentFormatterTest { testScope.runTest { `when`(repository.getDeviceSettingsConfig(cachedDevice)).thenReturn(null) - underTest.updateLayout() + underTest.updateLayout(FragmentTypeModel.DeviceDetailsMainFragment) - assertThat(getDisplayedPreferences().map { it.key }) + assertThat(getDisplayedPreferences().mapNotNull { it.key }) .containsExactly("bluetooth_device_header", "action_buttons", "keyboard_settings") } } @@ -157,12 +156,11 @@ class DeviceDetailsFragmentFormatterTest { DeviceSettingId.DEVICE_SETTING_ID_KEYBOARD_SETTINGS, "keyboard_settings"), ), - listOf(), - "footer")) + listOf())) - underTest.updateLayout() + underTest.updateLayout(FragmentTypeModel.DeviceDetailsMainFragment) - assertThat(getDisplayedPreferences().map { it.key }) + assertThat(getDisplayedPreferences().mapNotNull { it.key }) .containsExactly("bluetooth_device_header", "keyboard_settings") } } @@ -183,8 +181,7 @@ class DeviceDetailsFragmentFormatterTest { DeviceSettingId.DEVICE_SETTING_ID_KEYBOARD_SETTINGS, "keyboard_settings"), ), - listOf(), - "footer")) + listOf())) `when`(repository.getDeviceSetting(cachedDevice, DeviceSettingId.DEVICE_SETTING_ID_ANC)) .thenReturn( flowOf( @@ -209,9 +206,9 @@ class DeviceDetailsFragmentFormatterTest { isAllowedChangingState = true, updateState = {}))) - underTest.updateLayout() + underTest.updateLayout(FragmentTypeModel.DeviceDetailsMainFragment) - assertThat(getDisplayedPreferences().map { it.key }) + assertThat(getDisplayedPreferences().mapNotNull { it.key }) .containsExactly( "bluetooth_device_header", "DEVICE_SETTING_${DeviceSettingId.DEVICE_SETTING_ID_ANC}", 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 a1fadb8b354..378f363f0dd 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 @@ -16,12 +16,14 @@ package com.android.settings.bluetooth.ui.viewmodel +import android.app.Application import android.bluetooth.BluetoothAdapter -import android.content.Context import android.graphics.Bitmap import androidx.test.core.app.ApplicationProvider import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractor import com.android.settings.bluetooth.ui.layout.DeviceSettingLayout +import com.android.settings.bluetooth.ui.model.DeviceSettingPreferenceModel +import com.android.settings.bluetooth.ui.model.FragmentTypeModel import com.android.settings.testutils.FakeFeatureFactory import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId @@ -44,8 +46,6 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.any -import org.mockito.ArgumentMatchers.eq import org.mockito.Mock import org.mockito.Mockito.times import org.mockito.Mockito.verify @@ -73,26 +73,23 @@ class BluetoothDeviceDetailsViewModelTest { @Before fun setUp() { - val context = ApplicationProvider.getApplicationContext() + val application = ApplicationProvider.getApplicationContext() featureFactory = FakeFeatureFactory.setupForTest() - `when`( - featureFactory.bluetoothFeatureProvider.getDeviceSettingRepository( - eq(context), eq(bluetoothAdapter), any())) - .thenReturn(repository) underTest = - BluetoothDeviceDetailsViewModel(repository, spatialAudioInteractor, cachedDevice) + BluetoothDeviceDetailsViewModel( + application, repository, spatialAudioInteractor, cachedDevice) } @Test - fun getItems_returnConfigMainItems() { + fun getItems_returnConfigMainMainItems() { testScope.runTest { `when`(repository.getDeviceSettingsConfig(cachedDevice)) .thenReturn( DeviceSettingConfigModel( - listOf(BUILTIN_SETTING_ITEM_1, BUILDIN_SETTING_ITEM_2), listOf(), "footer")) + listOf(BUILTIN_SETTING_ITEM_1, BUILDIN_SETTING_ITEM_2), listOf())) - val keys = underTest.getItems() + val keys = underTest.getItems(FragmentTypeModel.DeviceDetailsMainFragment) assertThat(keys).containsExactly(BUILTIN_SETTING_ITEM_1, BUILDIN_SETTING_ITEM_2) } @@ -110,19 +107,18 @@ class BluetoothDeviceDetailsViewModelTest { BUILTIN_SETTING_ITEM_1, buildRemoteSettingItem(remoteSettingId1), ), - listOf(), - "footer")) + listOf())) `when`(repository.getDeviceSetting(cachedDevice, remoteSettingId1)) .thenReturn(flowOf(pref)) - var deviceSetting: DeviceSettingModel? = null + var deviceSettingPreference: DeviceSettingPreferenceModel? = null underTest .getDeviceSetting(cachedDevice, remoteSettingId1) - .onEach { deviceSetting = it } + .onEach { deviceSettingPreference = it } .launchIn(testScope.backgroundScope) runCurrent() - assertThat(deviceSetting).isSameInstanceAs(pref) + assertThat(deviceSettingPreference?.id).isEqualTo(pref.id) verify(repository, times(1)).getDeviceSetting(cachedDevice, remoteSettingId1) } } @@ -141,19 +137,18 @@ class BluetoothDeviceDetailsViewModelTest { buildRemoteSettingItem( DeviceSettingId.DEVICE_SETTING_ID_SPATIAL_AUDIO_MULTI_TOGGLE), ), - listOf(), - "footer")) + listOf())) `when`(spatialAudioInteractor.getDeviceSetting(cachedDevice)).thenReturn(flowOf(pref)) - var deviceSetting: DeviceSettingModel? = null + var deviceSettingPreference: DeviceSettingPreferenceModel? = null underTest .getDeviceSetting( cachedDevice, DeviceSettingId.DEVICE_SETTING_ID_SPATIAL_AUDIO_MULTI_TOGGLE) - .onEach { deviceSetting = it } + .onEach { deviceSettingPreference = it } .launchIn(testScope.backgroundScope) runCurrent() - assertThat(deviceSetting).isSameInstanceAs(pref) + assertThat(deviceSettingPreference?.id).isEqualTo(pref.id) verify(spatialAudioInteractor, times(1)).getDeviceSetting(cachedDevice) } } @@ -164,9 +159,9 @@ class BluetoothDeviceDetailsViewModelTest { `when`(repository.getDeviceSettingsConfig(cachedDevice)) .thenReturn( DeviceSettingConfigModel( - listOf(BUILTIN_SETTING_ITEM_1, BUILDIN_SETTING_ITEM_2), listOf(), "footer")) + listOf(BUILTIN_SETTING_ITEM_1, BUILDIN_SETTING_ITEM_2), listOf())) - val layout = underTest.getLayout()!! + val layout = underTest.getLayout(FragmentTypeModel.DeviceDetailsMainFragment)!! assertThat(getLatestLayout(layout)) .isEqualTo( @@ -191,8 +186,7 @@ class BluetoothDeviceDetailsViewModelTest { buildRemoteSettingItem(remoteSettingId2), buildRemoteSettingItem(remoteSettingId3), ), - listOf(), - "footer")) + listOf())) `when`(repository.getDeviceSetting(cachedDevice, remoteSettingId1)) .thenReturn(flowOf(buildMultiTogglePreference(remoteSettingId1))) `when`(repository.getDeviceSetting(cachedDevice, remoteSettingId2)) @@ -200,7 +194,7 @@ class BluetoothDeviceDetailsViewModelTest { `when`(repository.getDeviceSetting(cachedDevice, remoteSettingId3)) .thenReturn(flowOf(buildActionSwitchPreference(remoteSettingId3))) - val layout = underTest.getLayout()!! + val layout = underTest.getLayout(FragmentTypeModel.DeviceDetailsMainFragment)!! assertThat(getLatestLayout(layout)) .isEqualTo(