Merge "Add device details more settings page" into main

This commit is contained in:
Haijie Hong
2024-08-20 09:43:30 +00:00
committed by Android (Google) Code Review
12 changed files with 428 additions and 118 deletions

View File

@@ -1864,6 +1864,10 @@
<string name="device_details_title">Device details</string>
<!-- Title for keyboard settings preferences. [CHAR LIMIT=50] -->
<string name="bluetooth_device_keyboard_settings_preference_title">Keyboard settings</string>
<!-- Title for more settings preferences. [CHAR LIMIT=50] -->
<string name="bluetooth_device_more_settings_preference_title">More settings</string>
<!-- Title for more settings summary. [CHAR LIMIT=50] -->
<string name="bluetooth_device_more_settings_preference_summary">Firmware updates, about, and more</string>
<!-- Title of the item to show device MAC address -->
<string name="bluetooth_device_mac_address">Device\'s Bluetooth address: <xliff:g id="address">%1$s</xliff:g></string>
<!-- Title of the items to show multuple devices MAC address [CHAR LIMIT=NONE]-->
@@ -1884,6 +1888,9 @@
<!-- Bluetooth device details companion apps. In the confirmation dialog for removing an associated app, this is the label on the button that will complete the disassociate action. [CHAR LIMIT=80] -->
<string name = "bluetooth_companion_app_remove_association_confirm_button">Disconnect app</string>
<!-- Title of device details screen [CHAR LIMIT=28]-->
<string name="device_details_more_settings">More settings</string>
<!-- Bluetooth developer settings: Maximum number of connected audio devices -->
<string name="bluetooth_max_connected_audio_devices_string">Maximum connected Bluetooth audio devices</string>
<!-- Bluetooth developer settings: Maximum number of connected audio devices -->

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
android:key="bluetooth_device_more_settings_screen"
android:title="@string/device_details_more_settings">
<PreferenceCategory
android:key="bluetooth_profiles"/>
</PreferenceScreen>

View File

@@ -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<String> keys = mFormatter.getVisiblePreferenceKeysForMainPage();
List<String> keys =
mFormatter.getVisiblePreferenceKeys(
FragmentTypeModel.DeviceDetailsMainFragment.INSTANCE);
Lifecycle lifecycle = getSettingsLifecycle();
if (keys == null || keys.contains(controller.getPreferenceKey())) {
super.addPreferenceController(controller);

View File

@@ -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<DeviceSettingModel.MultiTogglePreference>,
preferenceModels: List<DeviceSettingPreferenceModel.MultiTogglePreference>,
) {
var settingIdForPopUp by remember { mutableStateOf<Int?>(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 =

View File

@@ -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<ToggleModel>,
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
}

View File

@@ -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
}

View File

@@ -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<String>?
fun getVisiblePreferenceKeys(fragmentType: FragmentTypeModel): List<String>?
/** 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<String>? = runBlocking {
override fun getVisiblePreferenceKeys(fragmentType: FragmentTypeModel): List<String>? =
runBlocking {
viewModel
.getItems()
.getItems(fragmentType)
?.filterIsInstance<DeviceSettingConfigItemModel.BuiltinItem>()
?.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<DeviceSettingConfigItemModel.BuiltinItem>()
@@ -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<DeviceSettingModel>())
flowOf(emptyList<DeviceSettingPreferenceModel>())
} 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))
}
is DeviceSettingPreferenceModel.FooterPreference -> {
buildFooterPreference(setting)
}
is DeviceSettingPreferenceModel.MoreSettingsPreference -> {
buildMoreSettingsPreference()
}
null -> {}
else -> {
Log.w(TAG, "Unknown preference type ${setting.id}, skip.")
}
}
}
else -> {
if (!settings.all { it is DeviceSettingModel.MultiTogglePreference }) {
if (!settings.all { it is DeviceSettingPreferenceModel.MultiTogglePreference }) {
return
}
buildMultiTogglePreference(
settings.filterIsInstance<DeviceSettingModel.MultiTogglePreference>())
settings.filterIsInstance<DeviceSettingPreferenceModel.MultiTogglePreference>())
}
}
}
@Composable
private fun buildMultiTogglePreference(prefs: List<DeviceSettingModel.MultiTogglePreference>) {
private fun buildMultiTogglePreference(
prefs: List<DeviceSettingPreferenceModel.MultiTogglePreference>
) {
MultiTogglePreferenceGroup(prefs)
}
@Composable
private fun buildActionSwitchPreference(model: DeviceSettingModel.ActionSwitchPreference) {
if (model.switchState != null) {
private fun buildSwitchPreference(model: DeviceSettingPreferenceModel.SwitchPreference) {
val switchPrefModel =
object : SwitchPreferenceModel {
override val title = model.title
override val summary = { model.summary ?: "" }
override val checked = { model.switchState?.checked }
override val checked = { model.checked }
override val onCheckedChange = { newChecked: Boolean ->
model.updateState?.invoke(
DeviceSettingStateModel.ActionSwitchPreferenceState(newChecked))
Unit
model.onCheckedChange(newChecked)
}
override val icon = @Composable { deviceSettingIcon(model) }
override val icon = @Composable { deviceSettingIcon(model.icon) }
}
if (model.intent != null) {
TwoTargetSwitchPreference(switchPrefModel) { context.startActivity(model.intent) }
if (model.onPrimaryClick != null) {
TwoTargetSwitchPreference(
switchPrefModel, primaryOnClick = model.onPrimaryClick::invoke)
} else {
SwitchPreference(switchPrefModel)
}
} else {
}
@Composable
private fun buildPlainPreference(model: DeviceSettingPreferenceModel.PlainPreference) {
SpaPreference(
object : PreferenceModel {
override val title = model.title
override val summary = { model.summary ?: "" }
override val onClick = {
model.intent?.let { context.startActivity(it) }
model.onClick?.invoke()
Unit
}
override val icon = @Composable { deviceSettingIcon(model) }
override val icon = @Composable { deviceSettingIcon(model.icon) }
})
}
}
@Composable
private fun deviceSettingIcon(model: DeviceSettingModel.ActionSwitchPreference) {
model.icon?.let { icon ->
Icon(icon, modifier = Modifier.size(SettingsDimension.itemIconSize))
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}"

View File

@@ -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<String>? =
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<AbstractPreferenceController> {
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"
}
}

View File

@@ -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<DeviceSettingConfigItemModel>? = items.await()?.mainItems
suspend fun getItems(fragment: FragmentTypeModel): List<DeviceSettingConfigItemModel>? =
when (fragment) {
is FragmentTypeModel.DeviceDetailsMainFragment -> items.await()?.mainItems
is FragmentTypeModel.DeviceDetailsMoreSettingsFragment ->
items.await()?.moreSettingsItems
}
fun getDeviceSetting(
cachedDevice: CachedBluetoothDevice,
@DeviceSettingId settingId: Int
): Flow<DeviceSettingModel?> {
): Flow<DeviceSettingPreferenceModel?> {
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<DeviceSettingConfigItemModel.AppProvidedItem>()
@@ -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 <T : ViewModel> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")
return BluetoothDeviceDetailsViewModel(
deviceSettingRepository, spatialAudioInteractor, cachedDevice)
application, deviceSettingRepository, spatialAudioInteractor, cachedDevice)
as T
}
}

View File

@@ -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);

View File

@@ -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}",

View File

@@ -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<Context>()
val application = ApplicationProvider.getApplicationContext<Application>()
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(