Add device details more settings page
BUG: 343317785
Test: atest DeviceDetailsFragmentFormatterTest
Flag: com.android.settings.flags.enable_bluetooth_device_details_polish
Change-Id: Ia9eff049e73e039ac5d0fb26096ab4e9add60315
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 939befe..c405e12 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -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 -->
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 0000000..4fb4aca
--- /dev/null
+++ b/res/xml/bluetooth_device_more_settings_fragment.xml
@@ -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>
diff --git a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java
index bd762a1..54250f5 100644
--- a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java
+++ b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java
@@ -48,6 +48,7 @@
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 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 @@
@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);
diff --git a/src/com/android/settings/bluetooth/ui/composable/MultiTogglePreferenceGroup.kt b/src/com/android/settings/bluetooth/ui/composable/MultiTogglePreferenceGroup.kt
index 8fe3c25..d29795e 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.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 @@
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 @@
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun dialog(
- multiTogglePreference: DeviceSettingModel.MultiTogglePreference,
+ multiTogglePreference: DeviceSettingPreferenceModel.MultiTogglePreference,
onDismiss: () -> Unit
) {
BasicAlertDialog(
@@ -179,7 +178,7 @@
}
@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 @@
}
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 @@
) {
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 0000000..6612591
--- /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<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
+}
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 0000000..19858c4
--- /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 b75579d..c933c75 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 @@
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 @@
ViewModelProvider(
fragment,
BluetoothDeviceDetailsViewModel.Factory(
+ fragment.requireActivity().application,
repository,
spatialAudioInteractor,
cachedDevice,
))
.get(BluetoothDeviceDetailsViewModel::class.java)
- override fun getVisiblePreferenceKeysForMainPage(): List<String>? = runBlocking {
- viewModel
- .getItems()
- ?.filterIsInstance<DeviceSettingConfigItemModel.BuiltinItem>()
- ?.mapNotNull { it.preferenceKey }
- }
+ override fun getVisiblePreferenceKeys(fragmentType: FragmentTypeModel): List<String>? =
+ runBlocking {
+ viewModel
+ .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 @@
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 @@
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 @@
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<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) {
- 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 0000000..c648a3e
--- /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<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"
+ }
+}
diff --git a/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModel.kt b/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModel.kt
index befff83..c85015c 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.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 @@
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 Factory(
+ private val application: Application,
private val deviceSettingRepository: DeviceSettingRepository,
private val spatialAudioInteractor: SpatialAudioInteractor,
private val cachedDevice: CachedBluetoothDevice,
@@ -110,7 +167,7 @@
override fun <T : ViewModel> create(modelClass: Class<T>): 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 19d0edd..c84d42c 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.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 @@
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 609d767..251b814 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.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.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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 a1fadb8..378f363 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.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 @@
@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 @@
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 @@
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 @@
`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 @@
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 @@
`when`(repository.getDeviceSetting(cachedDevice, remoteSettingId3))
.thenReturn(flowOf(buildActionSwitchPreference(remoteSettingId3)))
- val layout = underTest.getLayout()!!
+ val layout = underTest.getLayout(FragmentTypeModel.DeviceDetailsMainFragment)!!
assertThat(getLatestLayout(layout))
.isEqualTo(