diff --git a/res/drawable/device_details_spotlight_preference_background.xml b/res/drawable/device_details_spotlight_preference_background.xml
new file mode 100644
index 00000000000..58a29a10dcd
--- /dev/null
+++ b/res/drawable/device_details_spotlight_preference_background.xml
@@ -0,0 +1,28 @@
+
+
+
+
+ -
+
+
+
+
+
+
diff --git a/res/layout/bluetooth_device_spotlight_preference.xml b/res/layout/bluetooth_device_spotlight_preference.xml
new file mode 100644
index 00000000000..ee2778a53ec
--- /dev/null
+++ b/res/layout/bluetooth_device_spotlight_preference.xml
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/com/android/settings/bluetooth/ui/layout/DeviceSettingLayout.kt b/src/com/android/settings/bluetooth/ui/layout/DeviceSettingLayout.kt
deleted file mode 100644
index 5987e5a2079..00000000000
--- a/src/com/android/settings/bluetooth/ui/layout/DeviceSettingLayout.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * 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.layout
-
-import kotlinx.coroutines.flow.Flow
-
-/** Represent the layout of device settings. */
-data class DeviceSettingLayout(val rows: List)
-
-/** Represent a row in the layout. */
-data class DeviceSettingLayoutRow(val columns: Flow>)
-
-/** Represent a column in a row. */
-data class DeviceSettingLayoutColumn(val settingId: Int, val highlighted: Boolean)
diff --git a/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt b/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt
index b7ec32eb8e6..0658b1da40d 100644
--- a/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt
+++ b/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt
@@ -21,35 +21,26 @@ import android.app.settings.SettingsEnums
import android.bluetooth.BluetoothAdapter
import android.content.Context
import android.content.Intent
+import android.graphics.drawable.Drawable
import android.os.Bundle
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
+import androidx.core.graphics.drawable.toDrawable
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
+import androidx.preference.PreferenceViewHolder
+import androidx.preference.SwitchPreferenceCompat
+import androidx.preference.TwoStatePreference
import com.android.settings.R
import com.android.settings.bluetooth.BlockingPrefWithSliceController
import com.android.settings.bluetooth.BluetoothDetailsProfilesController
-import com.android.settings.bluetooth.ui.composable.Icon
import com.android.settings.bluetooth.ui.composable.MultiTogglePreference
-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
@@ -58,6 +49,7 @@ import com.android.settings.core.SubSettingLauncher
import com.android.settings.dashboard.DashboardFragment
import com.android.settings.overlay.FeatureFactory
import com.android.settings.spa.preference.ComposePreference
+import com.android.settingslib.PrimarySwitchPreference
import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingActionModel
@@ -67,24 +59,15 @@ import com.android.settingslib.core.AbstractPreferenceController
import com.android.settingslib.core.lifecycle.LifecycleObserver
import com.android.settingslib.core.lifecycle.events.OnPause
import com.android.settingslib.core.lifecycle.events.OnStop
-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 com.android.settingslib.widget.FooterPreference
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.emitAll
-import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
@@ -105,7 +88,7 @@ interface DeviceDetailsFragmentFormatter {
@OptIn(ExperimentalCoroutinesApi::class)
class DeviceDetailsFragmentFormatterImpl(
private val context: Context,
- private val fragment: DashboardFragment,
+ private val dashboardFragment: DashboardFragment,
controllers: List,
private val bluetoothAdapter: BluetoothAdapter,
private val cachedDevice: CachedBluetoothDevice,
@@ -120,32 +103,30 @@ class DeviceDetailsFragmentFormatterImpl(
private val viewModel: BluetoothDeviceDetailsViewModel =
ViewModelProvider(
- fragment,
- BluetoothDeviceDetailsViewModel.Factory(
- fragment.requireActivity().application,
- bluetoothAdapter,
- cachedDevice,
- backgroundCoroutineContext,
- ),
- )
+ dashboardFragment,
+ BluetoothDeviceDetailsViewModel.Factory(
+ dashboardFragment.requireActivity().application,
+ bluetoothAdapter,
+ cachedDevice,
+ backgroundCoroutineContext,
+ ),
+ )
.get(BluetoothDeviceDetailsViewModel::class.java)
/** Updates bluetooth device details fragment layout. */
override fun updateLayout(fragmentType: FragmentTypeModel) {
- fragment.setLoading(true, false)
+ dashboardFragment.setLoading(true, false)
isLoading = true
- fragment.lifecycleScope.launch { updateLayoutInternal(fragmentType) }
+ dashboardFragment.lifecycleScope.launch { updateLayoutInternal(fragmentType) }
}
private suspend fun updateLayoutInternal(fragmentType: FragmentTypeModel) {
- val items = viewModel.getItems(fragmentType) ?: run {
- fragment.setLoading(false, false)
- return
- }
- val layout = viewModel.getLayout(fragmentType) ?: run {
- fragment.setLoading(false, false)
- return
- }
+ val items =
+ viewModel.getItems(fragmentType)
+ ?: run {
+ dashboardFragment.setLoading(false, false)
+ return
+ }
val prefKeyToSettingId =
items
@@ -153,14 +134,14 @@ class DeviceDetailsFragmentFormatterImpl(
.associateBy({ it.preferenceKey }, { it.settingId })
val settingIdToXmlPreferences: MutableMap = HashMap()
- for (i in 0 until fragment.preferenceScreen.preferenceCount) {
- val pref = fragment.preferenceScreen.getPreference(i)
+ for (i in 0 until dashboardFragment.preferenceScreen.preferenceCount) {
+ val pref = dashboardFragment.preferenceScreen.getPreference(i)
prefKeyToSettingId[pref.key]?.let { id -> settingIdToXmlPreferences[id] = pref }
if (pref.key !in prefKeyToSettingId) {
getController(pref.key)?.let { disableController(it) }
}
}
- fragment.preferenceScreen.removeAll()
+ dashboardFragment.preferenceScreen.removeAll()
for (job in prefVisibilityJobs) {
job.cancel()
}
@@ -170,53 +151,83 @@ class DeviceDetailsFragmentFormatterImpl(
val settingId = settingItem.settingId
if (settingIdToXmlPreferences.containsKey(settingId)) {
val pref = settingIdToXmlPreferences[settingId]!!.apply { order = row }
- fragment.preferenceScreen.addPreference(pref)
+ dashboardFragment.preferenceScreen.addPreference(pref)
} else {
val prefKey = getPreferenceKey(settingId)
+
prefVisibilityJobs.add(
- getDevicesSettingForRow(layout, row)
- .onEach { logItemShown(prefKey, it.isNotEmpty()) }
- .launchIn(fragment.lifecycleScope)
+ viewModel
+ .getDeviceSetting(cachedDevice, settingId)
+ .onEach { logItemShown(prefKey, it != null) }
+ .launchIn(dashboardFragment.lifecycleScope)
)
- val pref =
- ComposePreference(context)
- .apply {
- key = prefKey
- order = row
+ if (settingId == DeviceSettingId.DEVICE_SETTING_ID_ANC) {
+ // TODO(b/399316980): replace it with SegmentedButtonPreference once it's ready.
+ val pref =
+ ComposePreference(context)
+ .apply {
+ key = prefKey
+ order = row
+ }
+ .also { pref ->
+ pref.setContent {
+ buildComposePreference(cachedDevice, settingId, prefKey)
+ }
+ }
+ dashboardFragment.preferenceScreen.addPreference(pref)
+ } else {
+ viewModel
+ .getDeviceSetting(cachedDevice, settingId)
+ .onEach {
+ val existedPref =
+ dashboardFragment.preferenceScreen.findPreference(
+ prefKey
+ )
+ val item =
+ it
+ ?: run {
+ existedPref?.let {
+ dashboardFragment.preferenceScreen.removePreference(
+ existedPref
+ )
+ }
+ return@onEach
+ }
+ buildPreference(existedPref, item, prefKey, settingItem.highlighted)
+ ?.apply {
+ key = prefKey
+ order = row
+ }
+ ?.also { dashboardFragment.preferenceScreen.addPreference(it) }
}
- .also { pref -> pref.setContent { buildPreference(layout, row, prefKey) } }
- fragment.preferenceScreen.addPreference(pref)
+ .launchIn(dashboardFragment.lifecycleScope)
+ }
}
}
- // TODO(b/343317785): figure out how to remove the foot preference.
- fragment.preferenceScreen.addPreference(ComposePreference(context).apply {
- order = 10000
- isEnabled = false
- isSelectable = false
- setContent { Spacer(modifier = Modifier.height(1.dp)) }
- })
for (row in items.indices) {
val settingItem = items[row]
val settingId = settingItem.settingId
- if (settingIdToXmlPreferences.containsKey(settingId)) {
- val pref = fragment.preferenceScreen.getPreference(row)
+ settingIdToXmlPreferences[settingId]?.let { pref ->
if (settingId == DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_PROFILES) {
(getController(pref.key) as? BluetoothDetailsProfilesController)?.run {
- if (settingItem is DeviceSettingConfigItemModel.BuiltinItem.BluetoothProfilesItem) {
+ if (
+ settingItem
+ is DeviceSettingConfigItemModel.BuiltinItem.BluetoothProfilesItem
+ ) {
setInvisibleProfiles(settingItem.invisibleProfiles)
setHasExtraSpace(false)
}
}
}
- getController(pref.key)?.displayPreference(fragment.preferenceScreen)
+ getController(pref.key)?.displayPreference(dashboardFragment.preferenceScreen)
logItemShown(pref.key, pref.isVisible)
}
}
- fragment.lifecycleScope.launch {
+ dashboardFragment.lifecycleScope.launch {
if (isLoading) {
- fragment.setLoading(false, false)
+ dashboardFragment.setLoading(false, false)
isLoading = false
}
}
@@ -236,87 +247,170 @@ class DeviceDetailsFragmentFormatterImpl(
} ?: emit(null)
}
- private fun getDevicesSettingForRow(
- layout: DeviceSettingLayout,
- row: Int,
- ): Flow> =
- layout.rows[row].columns.flatMapLatest { columns ->
- if (columns.isEmpty()) {
- flowOf(emptyList())
- } else {
- combine(
- columns.map { column ->
- viewModel.getDeviceSetting(cachedDevice, column.settingId)
- }
- ) {
- it.toList().filterNotNull()
+ private fun buildPreference(
+ existedPref: Preference?,
+ model: DeviceSettingPreferenceModel,
+ prefKey: String,
+ highlighted: Boolean,
+ ): Preference? =
+ when (model) {
+ is DeviceSettingPreferenceModel.PlainPreference -> {
+ val pref =
+ existedPref
+ ?: run {
+ if (highlighted) SpotlightPreference(context) else Preference(context)
+ }
+ pref.apply {
+ title = model.title
+ summary = model.summary
+ icon = getDrawable(model.icon)
+ onPreferenceClickListener =
+ object : Preference.OnPreferenceClickListener {
+ override fun onPreferenceClick(p: Preference): Boolean {
+ logItemClick(prefKey, EVENT_CLICK_PRIMARY)
+ model.action?.let { triggerAction(it) }
+ return true
+ }
+ }
}
}
+ is DeviceSettingPreferenceModel.SwitchPreference ->
+ if (model.action == null) {
+ val pref =
+ existedPref as? SwitchPreferenceCompat ?: SwitchPreferenceCompat(context)
+ pref.apply {
+ title = model.title
+ summary = model.summary
+ icon = getDrawable(model.icon)
+ isChecked = model.checked
+ isEnabled = !model.disabled
+ onPreferenceChangeListener =
+ object : Preference.OnPreferenceChangeListener {
+ override fun onPreferenceChange(
+ p: Preference,
+ value: Any?,
+ ): Boolean {
+ (p as? TwoStatePreference)?.let { newState ->
+ val newState = value as? Boolean ?: return false
+ logItemClick(
+ prefKey,
+ if (newState) EVENT_SWITCH_ON else EVENT_SWITCH_OFF,
+ )
+ isEnabled = false
+ model.onCheckedChange.invoke(newState)
+ }
+ return false
+ }
+ }
+ }
+ } else {
+ val pref =
+ existedPref as? PrimarySwitchPreference ?: PrimarySwitchPreference(context)
+ pref.apply {
+ title = model.title
+ summary = model.summary
+ icon = getDrawable(model.icon)
+ isChecked = model.checked
+ isEnabled = !model.disabled
+ isSwitchEnabled = !model.disabled
+ onPreferenceClickListener =
+ object : Preference.OnPreferenceClickListener {
+ override fun onPreferenceClick(p: Preference): Boolean {
+ logItemClick(prefKey, EVENT_CLICK_PRIMARY)
+ triggerAction(model.action)
+ return true
+ }
+ }
+ onPreferenceChangeListener =
+ object : Preference.OnPreferenceChangeListener {
+ override fun onPreferenceChange(
+ p: Preference,
+ value: Any?,
+ ): Boolean {
+ val newState = value as? Boolean ?: return false
+ logItemClick(
+ prefKey,
+ if (newState) EVENT_SWITCH_ON else EVENT_SWITCH_OFF,
+ )
+ isSwitchEnabled = false
+ model.onCheckedChange.invoke(newState)
+ return false
+ }
+ }
+ }
+ }
+
+ is DeviceSettingPreferenceModel.MultiTogglePreference -> {
+ // TODO(b/399316980): implemented it by SegmentedButtonPreference
+ null
+ }
+ is DeviceSettingPreferenceModel.FooterPreference -> {
+ val pref = existedPref as? FooterPreference ?: FooterPreference(context)
+ pref.apply { title = model.footerText }
+ }
+ is DeviceSettingPreferenceModel.MoreSettingsPreference -> {
+ val pref = existedPref ?: Preference(context)
+ pref.apply {
+ title =
+ context.getString(R.string.bluetooth_device_more_settings_preference_title)
+ summary =
+ context.getString(
+ R.string.bluetooth_device_more_settings_preference_summary
+ )
+ icon = context.getDrawable(R.drawable.ic_chevron_right_24dp)
+ onPreferenceClickListener =
+ object : Preference.OnPreferenceClickListener {
+ override fun onPreferenceClick(p: Preference): Boolean {
+ logItemClick(prefKey, EVENT_CLICK_PRIMARY)
+ SubSettingLauncher(context)
+ .setDestination(
+ DeviceDetailsMoreSettingsFragment::class.java.name
+ )
+ .setSourceMetricsCategory(
+ dashboardFragment.getMetricsCategory()
+ )
+ .setArguments(
+ Bundle().apply {
+ putString(KEY_DEVICE_ADDRESS, cachedDevice.address)
+ }
+ )
+ .launch()
+ return true
+ }
+ }
+ }
+ }
+ is DeviceSettingPreferenceModel.HelpPreference -> {
+ null
+ }
}
- @Composable
- private fun buildPreference(layout: DeviceSettingLayout, row: Int, prefKey: String) {
- val contents by
- remember(row) { getDevicesSettingForRow(layout, row) }
- .collectAsStateWithLifecycle(initialValue = listOf())
-
- val highlighted by
- remember(row) {
- layout.rows[row].columns.map { columns -> columns.any { it.highlighted } }
+ private fun getDrawable(deviceSettingIcon: DeviceSettingIcon?): Drawable? =
+ when (deviceSettingIcon) {
+ is DeviceSettingIcon.BitmapIcon ->
+ deviceSettingIcon.bitmap.toDrawable(context.resources)
+ is DeviceSettingIcon.ResourceIcon -> context.getDrawable(deviceSettingIcon.resId)
+ null -> null
}
- .collectAsStateWithLifecycle(initialValue = false)
+
+ @Composable
+ private fun buildComposePreference(
+ cachedDevice: CachedBluetoothDevice,
+ settingId: Int,
+ prefKey: String,
+ ) {
+ val contents by
+ remember(settingId) { viewModel.getDeviceSetting(cachedDevice, settingId) }
+ .collectAsStateWithLifecycle(initialValue = null)
val settings = contents
- AnimatedVisibility(visible = settings.isNotEmpty(), enter = fadeIn(), exit = fadeOut()) {
- Box {
- Box(
- modifier =
- Modifier.matchParentSize()
- .padding(16.dp, 0.dp, 8.dp, 0.dp)
- .background(
- color =
- if (highlighted) {
- MaterialTheme.colorScheme.primaryContainer
- } else {
- Color.Transparent
- },
- shape = RoundedCornerShape(28.dp),
- )
- ) {}
- buildPreferences(settings, prefKey)
+ AnimatedVisibility(visible = settings != null, enter = fadeIn(), exit = fadeOut()) {
+ (settings as? DeviceSettingPreferenceModel.MultiTogglePreference)?.let {
+ buildMultiTogglePreference(it, prefKey)
}
}
}
- @Composable
- fun buildPreferences(settings: List, prefKey: String) {
- when (settings.size) {
- 0 -> {}
- 1 -> {
- when (val setting = settings[0]) {
- is DeviceSettingPreferenceModel.PlainPreference -> {
- buildPlainPreference(setting, prefKey)
- }
- is DeviceSettingPreferenceModel.SwitchPreference -> {
- buildSwitchPreference(setting, prefKey)
- }
- is DeviceSettingPreferenceModel.MultiTogglePreference -> {
- buildMultiTogglePreference(setting, prefKey)
- }
- is DeviceSettingPreferenceModel.FooterPreference -> {
- buildFooterPreference(setting)
- }
- is DeviceSettingPreferenceModel.MoreSettingsPreference -> {
- buildMoreSettingsPreference(prefKey)
- }
- is DeviceSettingPreferenceModel.HelpPreference -> {}
- null -> {}
- }
- }
- else -> {}
- }
- }
-
@Composable
private fun buildMultiTogglePreference(
pref: DeviceSettingPreferenceModel.MultiTogglePreference,
@@ -332,107 +426,6 @@ class DeviceDetailsFragmentFormatterImpl(
)
}
- @Composable
- private fun buildSwitchPreference(
- model: DeviceSettingPreferenceModel.SwitchPreference,
- prefKey: String,
- ) {
- val switchPrefModel =
- object : SwitchPreferenceModel {
- override val title = model.title
- override val summary = { model.summary ?: "" }
- override val checked = { model.checked }
- override val onCheckedChange = { newState: Boolean ->
- logItemClick(prefKey, if (newState) EVENT_SWITCH_ON else EVENT_SWITCH_OFF)
- model.onCheckedChange(newState)
- }
- override val changeable = { !model.disabled }
- override val icon: (@Composable () -> Unit)?
- get() {
- if (model.icon == null) {
- return null
- }
- return { deviceSettingIcon(model.icon) }
- }
- }
- if (model.action != null) {
- TwoTargetSwitchPreference(
- switchPrefModel,
- primaryOnClick = {
- logItemClick(prefKey, EVENT_CLICK_PRIMARY)
- triggerAction(model.action)
- },
- primaryEnabled = { !model.disabled },
- )
- } else {
- SwitchPreference(switchPrefModel)
- }
- }
-
- @Composable
- private fun buildPlainPreference(
- model: DeviceSettingPreferenceModel.PlainPreference,
- prefKey: String,
- ) {
- SpaPreference(
- object : PreferenceModel {
- override val title = model.title
- override val summary = { model.summary ?: "" }
- override val onClick = {
- logItemClick(prefKey, EVENT_CLICK_PRIMARY)
- model.action?.let { triggerAction(it) }
- Unit
- }
- override val icon: (@Composable () -> Unit)?
- get() {
- if (model.icon == null) {
- return null
- }
- return { deviceSettingIcon(model.icon) }
- }
- }
- )
- }
-
- @Composable
- fun buildMoreSettingsPreference(prefKey: String) {
- 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 = {
- logItemClick(prefKey, EVENT_CLICK_PRIMARY)
- 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(
- DeviceSettingIcon.ResourceIcon(R.drawable.ic_chevron_right_24dp)
- )
- }
- }
- )
- }
-
- @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 logItemClick(preferenceKey: String, value: Int = 0) {
logAction(preferenceKey, SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_ITEM_CLICKED, value)
}
@@ -452,7 +445,7 @@ class DeviceDetailsFragmentFormatterImpl(
if (it) EVENT_VISIBLE else EVENT_INVISIBLE,
)
}
- .launchIn(fragment.lifecycleScope)
+ .launchIn(dashboardFragment.lifecycleScope)
}
}
.value = visible
@@ -485,7 +478,7 @@ class DeviceDetailsFragmentFormatterImpl(
private fun disableController(controller: AbstractPreferenceController) {
if (controller is LifecycleObserver) {
- fragment.settingsLifecycle.removeObserver(controller as LifecycleObserver)
+ dashboardFragment.settingsLifecycle.removeObserver(controller as LifecycleObserver)
}
if (controller is BlockingPrefWithSliceController) {
@@ -504,6 +497,19 @@ class DeviceDetailsFragmentFormatterImpl(
private fun getPreferenceKey(settingId: Int) = "DEVICE_SETTING_${settingId}"
+ private class SpotlightPreference(context: Context) : Preference(context) {
+
+ init {
+ layoutResource = R.layout.bluetooth_device_spotlight_preference
+ }
+
+ override fun onBindViewHolder(holder: PreferenceViewHolder) {
+ super.onBindViewHolder(holder)
+ holder.isDividerAllowedBelow = false
+ holder.isDividerAllowedAbove = false
+ }
+ }
+
private companion object {
const val TAG = "DeviceDetailsFormatter"
const val EVENT_SWITCH_OFF = 0
diff --git a/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModel.kt b/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModel.kt
index 8d3b8539b98..5434ec98c55 100644
--- a/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModel.kt
+++ b/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModel.kt
@@ -23,9 +23,6 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.android.settings.R
-import com.android.settings.bluetooth.ui.layout.DeviceSettingLayout
-import com.android.settings.bluetooth.ui.layout.DeviceSettingLayoutColumn
-import com.android.settings.bluetooth.ui.layout.DeviceSettingLayoutRow
import com.android.settings.bluetooth.ui.model.DeviceSettingPreferenceModel
import com.android.settings.bluetooth.ui.model.FragmentTypeModel
import com.android.settings.overlay.FeatureFactory.Companion.featureFactory
@@ -39,11 +36,8 @@ import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
class BluetoothDeviceDetailsViewModel(
private val application: Application,
@@ -141,43 +135,6 @@ class BluetoothDeviceDetailsViewModel(
}
}
- suspend fun getLayout(fragment: FragmentTypeModel): DeviceSettingLayout? {
- val configItems = getItems(fragment) ?: return null
- val idToDeviceSetting =
- configItems
- .filterIsInstance()
- .associateBy({ it.settingId }, { getDeviceSetting(cachedDevice, it.settingId) })
-
- val configDeviceSetting =
- configItems.map { idToDeviceSetting[it.settingId] ?: flowOf(null) }
- val positionToSettingIds =
- combine(configDeviceSetting) { settings ->
- val positionMapping = mutableMapOf>()
- for (i in settings.indices) {
- val configItem = configItems[i]
- val setting = settings[i]
- val isXmlPreference = configItem is DeviceSettingConfigItemModel.BuiltinItem
- if (!isXmlPreference && setting == null) {
- continue
- }
- positionMapping[i] =
- listOf(
- DeviceSettingLayoutColumn(
- configItem.settingId,
- configItem.highlighted,
- )
- )
- }
- positionMapping
- }
- .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), initialValue = mapOf())
- return DeviceSettingLayout(
- configItems.indices.map { idx ->
- DeviceSettingLayoutRow(positionToSettingIds.map { it[idx] ?: emptyList() })
- }
- )
- }
-
class Factory(
private val application: Application,
private val bluetoothAdapter: BluetoothAdapter,
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 d0bd27d7a6e..1eb15e50f62 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
@@ -16,7 +16,7 @@
package com.android.settings.bluetooth.ui.view
-import android.app.settings.SettingsEnums;
+import android.app.settings.SettingsEnums
import android.bluetooth.BluetoothAdapter
import android.content.Context
import android.content.Intent
@@ -25,6 +25,7 @@ import androidx.fragment.app.FragmentActivity
import androidx.preference.Preference
import androidx.preference.PreferenceManager
import androidx.preference.PreferenceScreen
+import androidx.preference.SwitchPreferenceCompat
import androidx.test.core.app.ApplicationProvider
import com.android.settings.bluetooth.ui.model.DeviceSettingPreferenceModel
import com.android.settings.bluetooth.ui.model.FragmentTypeModel
@@ -33,6 +34,7 @@ import com.android.settings.testutils.FakeFeatureFactory
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.DeviceSettingActionModel
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigModel
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingIcon
@@ -58,14 +60,15 @@ import org.mockito.Mock
import org.mockito.Mockito.any
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
+import org.mockito.Spy
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.doNothing
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.shadows.ShadowLooper
import org.robolectric.shadows.ShadowLooper.shadowMainLooper
-
@ExperimentalCoroutinesApi
@RunWith(RobolectricTestRunner::class)
class DeviceDetailsFragmentFormatterTest {
@@ -78,7 +81,7 @@ class DeviceDetailsFragmentFormatterTest {
@Mock private lateinit var headerController: AbstractPreferenceController
@Mock private lateinit var buttonController: AbstractPreferenceController
- private lateinit var context: Context
+ @Spy private val context: Context = ApplicationProvider.getApplicationContext()
private lateinit var fragment: TestFragment
private lateinit var underTest: DeviceDetailsFragmentFormatter
private lateinit var featureFactory: FakeFeatureFactory
@@ -87,11 +90,15 @@ class DeviceDetailsFragmentFormatterTest {
@Before
fun setUp() {
- context = ApplicationProvider.getApplicationContext()
featureFactory = FakeFeatureFactory.setupForTest()
+ doNothing().`when`(context).startActivity(any(Intent::class.java))
`when`(
featureFactory.bluetoothFeatureProvider.getDeviceSettingRepository(
- eq(context), eq(bluetoothAdapter), any()))
+ any(),
+ eq(bluetoothAdapter),
+ any(),
+ )
+ )
.thenReturn(repository)
fragmentActivity = Robolectric.setupActivity(FragmentActivity::class.java)
assertThat(fragmentActivity.applicationContext).isNotNull()
@@ -115,7 +122,8 @@ class DeviceDetailsFragmentFormatterTest {
listOf(profileController, headerController, buttonController),
bluetoothAdapter,
cachedDevice,
- testScope.testScheduler)
+ testScope.testScheduler,
+ )
}
@Test
@@ -124,11 +132,16 @@ class DeviceDetailsFragmentFormatterTest {
`when`(repository.getDeviceSettingsConfig(cachedDevice))
.thenReturn(
DeviceSettingConfigModel(
- listOf(), listOf(), DeviceSettingConfigItemModel.AppProvidedItem(12345, false)))
- val intent = Intent().apply {
- setAction(Intent.ACTION_VIEW)
- setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- }
+ listOf(),
+ listOf(),
+ DeviceSettingConfigItemModel.AppProvidedItem(12345, false),
+ )
+ )
+ val intent =
+ Intent().apply {
+ setAction(Intent.ACTION_VIEW)
+ setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
`when`(repository.getDeviceSetting(cachedDevice, 12345))
.thenReturn(
flowOf(
@@ -136,12 +149,15 @@ class DeviceDetailsFragmentFormatterTest {
cachedDevice = cachedDevice,
id = 12345,
intent = intent,
- )))
+ )
+ )
+ )
var helpPreference: DeviceSettingPreferenceModel.HelpPreference? = null
- underTest.getMenuItem(FragmentTypeModel.DeviceDetailsMoreSettingsFragment).onEach {
- helpPreference = it
- }.launchIn(testScope.backgroundScope)
+ underTest
+ .getMenuItem(FragmentTypeModel.DeviceDetailsMoreSettingsFragment)
+ .onEach { helpPreference = it }
+ .launchIn(testScope.backgroundScope)
delay(100)
runCurrent()
ShadowLooper.idleMainLooper()
@@ -171,13 +187,19 @@ class DeviceDetailsFragmentFormatterTest {
listOf(
DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_HEADER,
- highlighted = false, preferenceKey = "bluetooth_device_header"),
+ highlighted = false,
+ preferenceKey = "bluetooth_device_header",
+ ),
DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_PROFILES,
- highlighted = false, preferenceKey = "bluetooth_profiles"),
+ highlighted = false,
+ preferenceKey = "bluetooth_profiles",
+ ),
),
listOf(),
- null))
+ null,
+ )
+ )
underTest.updateLayout(FragmentTypeModel.DeviceDetailsMainFragment)
runCurrent()
@@ -189,13 +211,17 @@ class DeviceDetailsFragmentFormatterTest {
SettingsEnums.PAGE_UNKNOWN,
SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_ITEM_SHOWN,
0,
- "bluetooth_device_header", 1)
+ "bluetooth_device_header",
+ 1,
+ )
verify(featureFactory.metricsFeatureProvider)
.action(
SettingsEnums.PAGE_UNKNOWN,
SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_ITEM_SHOWN,
0,
- "bluetooth_profiles", 1)
+ "bluetooth_profiles",
+ 1,
+ )
}
}
@@ -209,16 +235,22 @@ class DeviceDetailsFragmentFormatterTest {
DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_HEADER,
highlighted = false,
- preferenceKey = "bluetooth_device_header"),
+ preferenceKey = "bluetooth_device_header",
+ ),
DeviceSettingConfigItemModel.AppProvidedItem(
- DeviceSettingId.DEVICE_SETTING_ID_ANC, highlighted = false),
+ DeviceSettingId.DEVICE_SETTING_ID_ANC,
+ highlighted = false,
+ ),
DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_PROFILES,
highlighted = false,
- preferenceKey = "bluetooth_profiles"),
+ preferenceKey = "bluetooth_profiles",
+ ),
),
listOf(),
- null))
+ null,
+ )
+ )
`when`(repository.getDeviceSetting(cachedDevice, DeviceSettingId.DEVICE_SETTING_ID_ANC))
.thenReturn(
flowOf(
@@ -231,11 +263,17 @@ class DeviceDetailsFragmentFormatterTest {
ToggleModel(
"",
DeviceSettingIcon.BitmapIcon(
- Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)))),
+ Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
+ ),
+ )
+ ),
isActive = true,
state = DeviceSettingStateModel.MultiTogglePreferenceState(0),
isAllowedChangingState = true,
- updateState = {})))
+ updateState = {},
+ )
+ )
+ )
underTest.updateLayout(FragmentTypeModel.DeviceDetailsMainFragment)
runCurrent()
@@ -244,13 +282,119 @@ class DeviceDetailsFragmentFormatterTest {
.containsExactly(
"bluetooth_device_header",
"DEVICE_SETTING_${DeviceSettingId.DEVICE_SETTING_ID_ANC}",
- "bluetooth_profiles")
+ "bluetooth_profiles",
+ )
verify(featureFactory.metricsFeatureProvider)
.action(
SettingsEnums.PAGE_UNKNOWN,
SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_ITEM_SHOWN,
0,
- "DEVICE_SETTING_${DeviceSettingId.DEVICE_SETTING_ID_ANC}", 1
+ "DEVICE_SETTING_${DeviceSettingId.DEVICE_SETTING_ID_ANC}",
+ 1,
+ )
+ }
+ }
+
+ @Test
+ fun updateLayout_plainPreferenceClicked() {
+ testScope.runTest {
+ val settingId = 12345
+ val intent = Intent("test_intent")
+ `when`(repository.getDeviceSettingsConfig(cachedDevice))
+ .thenReturn(
+ DeviceSettingConfigModel(
+ listOf(
+ DeviceSettingConfigItemModel.AppProvidedItem(
+ settingId,
+ highlighted = false,
+ )
+ ),
+ listOf(),
+ null,
+ )
+ )
+
+ `when`(repository.getDeviceSetting(cachedDevice, settingId))
+ .thenReturn(
+ flowOf(
+ DeviceSettingModel.ActionSwitchPreference(
+ cachedDevice = cachedDevice,
+ id = settingId,
+ title = "title",
+ summary = "summary",
+ icon = null,
+ action = DeviceSettingActionModel.IntentAction(intent),
+ )
+ )
+ )
+
+ underTest.updateLayout(FragmentTypeModel.DeviceDetailsMainFragment)
+ runCurrent()
+ val displayedPrefs = getDisplayedPreferences()
+ displayedPrefs[0].performClick()
+
+ assertThat(displayedPrefs).hasSize(1)
+ verify(context).startActivity(intent)
+ verify(featureFactory.metricsFeatureProvider)
+ .action(
+ SettingsEnums.PAGE_UNKNOWN,
+ SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_ITEM_CLICKED,
+ 0,
+ "DEVICE_SETTING_$settingId",
+ 2,
+ )
+ }
+ }
+
+ @Test
+ fun updateLayout_switchPreferenceClicked() {
+ val settingId = 12345
+ testScope.runTest {
+ `when`(repository.getDeviceSettingsConfig(cachedDevice))
+ .thenReturn(
+ DeviceSettingConfigModel(
+ listOf(
+ DeviceSettingConfigItemModel.AppProvidedItem(
+ settingId,
+ highlighted = false,
+ )
+ ),
+ listOf(),
+ null,
+ )
+ )
+
+ `when`(repository.getDeviceSetting(cachedDevice, settingId))
+ .thenReturn(
+ flowOf(
+ DeviceSettingModel.ActionSwitchPreference(
+ cachedDevice = cachedDevice,
+ id = settingId,
+ title = "title",
+ summary = "summary",
+ icon = null,
+ action = null,
+ switchState = DeviceSettingStateModel.ActionSwitchPreferenceState(true),
+ isAllowedChangingState = true,
+ updateState = {},
+ )
+ )
+ )
+
+ underTest.updateLayout(FragmentTypeModel.DeviceDetailsMainFragment)
+ runCurrent()
+ val displayedPrefs = getDisplayedPreferences()
+ displayedPrefs[0].performClick()
+
+ assertThat(displayedPrefs).hasSize(1)
+ assertThat(displayedPrefs[0]).isInstanceOf(SwitchPreferenceCompat::class.java)
+ verify(featureFactory.metricsFeatureProvider)
+ .action(
+ SettingsEnums.PAGE_UNKNOWN,
+ SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_ITEM_CLICKED,
+ 0,
+ "DEVICE_SETTING_$settingId",
+ 0,
)
}
}
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 caeea942f62..bf66a04cede 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
@@ -20,7 +20,6 @@ import android.app.Application
import android.bluetooth.BluetoothAdapter
import android.graphics.Bitmap
import androidx.test.core.app.ApplicationProvider
-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
@@ -164,74 +163,6 @@ class BluetoothDeviceDetailsViewModelTest {
}
}
- @Test
- fun getLayout_builtinDeviceSettings() {
- testScope.runTest {
- `when`(repository.getDeviceSettingsConfig(cachedDevice))
- .thenReturn(
- DeviceSettingConfigModel(
- listOf(BUILTIN_SETTING_ITEM_1, BUILDIN_SETTING_ITEM_2), listOf(), null))
-
- val layout = underTest.getLayout(FragmentTypeModel.DeviceDetailsMainFragment)!!
-
- assertThat(getLatestLayout(layout))
- .isEqualTo(
- listOf(
- listOf(DeviceSettingId.DEVICE_SETTING_ID_HEADER),
- listOf(DeviceSettingId.DEVICE_SETTING_ID_ACTION_BUTTONS)))
- }
- }
-
- @Test
- fun getLayout_remoteDeviceSettings() {
- val remoteSettingId1 = 10001
- val remoteSettingId2 = 10002
- val remoteSettingId3 = 10003
- testScope.runTest {
- `when`(repository.getDeviceSettingsConfig(cachedDevice))
- .thenReturn(
- DeviceSettingConfigModel(
- listOf(
- BUILTIN_SETTING_ITEM_1,
- buildRemoteSettingItem(remoteSettingId1),
- buildRemoteSettingItem(remoteSettingId2),
- buildRemoteSettingItem(remoteSettingId3),
- ),
- listOf(),
- null))
- `when`(repository.getDeviceSetting(cachedDevice, remoteSettingId1))
- .thenReturn(flowOf(buildMultiTogglePreference(remoteSettingId1)))
- `when`(repository.getDeviceSetting(cachedDevice, remoteSettingId2))
- .thenReturn(flowOf(buildMultiTogglePreference(remoteSettingId2)))
- `when`(repository.getDeviceSetting(cachedDevice, remoteSettingId3))
- .thenReturn(flowOf(buildActionSwitchPreference(remoteSettingId3)))
-
- val layout = underTest.getLayout(FragmentTypeModel.DeviceDetailsMainFragment)!!
-
- assertThat(getLatestLayout(layout))
- .isEqualTo(
- listOf(
- listOf(DeviceSettingId.DEVICE_SETTING_ID_HEADER),
- listOf(remoteSettingId1),
- listOf(remoteSettingId2),
- listOf(remoteSettingId3),
- ))
- }
- }
-
- private fun getLatestLayout(layout: DeviceSettingLayout): List> {
- val latestLayout = MutableList(layout.rows.size) { emptyList() }
- for (i in layout.rows.indices) {
- layout.rows[i]
- .columns
- .onEach { latestLayout[i] = it.map { c -> c.settingId } }
- .launchIn(testScope.backgroundScope)
- }
-
- testScope.runCurrent()
- return latestLayout.filter { !it.isEmpty() }.toList()
- }
-
private fun buildMultiTogglePreference(settingId: Int) =
DeviceSettingModel.MultiTogglePreference(
cachedDevice,