Merge "Reimplement device details UI without ComposePreference" into main

This commit is contained in:
Haijie Hong
2025-03-11 01:29:30 -07:00
committed by Android (Google) Code Review
7 changed files with 522 additions and 411 deletions

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2025 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.
-->
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?android:colorControlHighlight">
<item>
<shape android:shape="rectangle">
<solid
android:color="@color/settingslib_materialColorSecondaryContainer" />
<corners
android:radius="28dp" />
</shape>
</item>
</ripple>

View File

@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2025 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.
-->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingEnd="?android:attr/scrollbarSize"
android:background="?android:attr/selectableItemBackground">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginHorizontal="16dp"
android:background="@drawable/device_details_spotlight_preference_background" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?android:attr/listPreferredItemHeight"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:gravity="center_vertical">
<ImageView
android:id="@+android:id/icon"
android:layout_width="24dip"
android:layout_height="24dip"
android:layout_gravity="center"
android:scaleType="center"
android:importantForAccessibility="no" />
<RelativeLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginEnd="6dp"
android:layout_marginTop="6dp"
android:layout_marginBottom="6dp"
android:layout_weight="1">
<TextView android:id="@+android:id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceLarge"
android:ellipsize="marquee"
android:fadingEdge="horizontal" />
<TextView android:id="@+android:id/summary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@android:id/title"
android:layout_alignStart="@android:id/title"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondary"
android:maxLines="4" />
</RelativeLayout>
</LinearLayout>
</FrameLayout>

View File

@@ -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<DeviceSettingLayoutRow>)
/** Represent a row in the layout. */
data class DeviceSettingLayoutRow(val columns: Flow<List<DeviceSettingLayoutColumn>>)
/** Represent a column in a row. */
data class DeviceSettingLayoutColumn(val settingId: Int, val highlighted: Boolean)

View File

@@ -21,35 +21,26 @@ import android.app.settings.SettingsEnums
import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothAdapter
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.drawable.Drawable
import android.os.Bundle import android.os.Bundle
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut 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.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.core.graphics.drawable.toDrawable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference 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.R
import com.android.settings.bluetooth.BlockingPrefWithSliceController import com.android.settings.bluetooth.BlockingPrefWithSliceController
import com.android.settings.bluetooth.BluetoothDetailsProfilesController 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.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.DeviceSettingPreferenceModel
import com.android.settings.bluetooth.ui.model.FragmentTypeModel 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.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.dashboard.DashboardFragment
import com.android.settings.overlay.FeatureFactory import com.android.settings.overlay.FeatureFactory
import com.android.settings.spa.preference.ComposePreference import com.android.settings.spa.preference.ComposePreference
import com.android.settingslib.PrimarySwitchPreference
import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingActionModel 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.LifecycleObserver
import com.android.settingslib.core.lifecycle.events.OnPause import com.android.settingslib.core.lifecycle.events.OnPause
import com.android.settingslib.core.lifecycle.events.OnStop import com.android.settingslib.core.lifecycle.events.OnStop
import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.widget.FooterPreference
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 kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@@ -105,7 +88,7 @@ interface DeviceDetailsFragmentFormatter {
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class DeviceDetailsFragmentFormatterImpl( class DeviceDetailsFragmentFormatterImpl(
private val context: Context, private val context: Context,
private val fragment: DashboardFragment, private val dashboardFragment: DashboardFragment,
controllers: List<AbstractPreferenceController>, controllers: List<AbstractPreferenceController>,
private val bluetoothAdapter: BluetoothAdapter, private val bluetoothAdapter: BluetoothAdapter,
private val cachedDevice: CachedBluetoothDevice, private val cachedDevice: CachedBluetoothDevice,
@@ -120,32 +103,30 @@ class DeviceDetailsFragmentFormatterImpl(
private val viewModel: BluetoothDeviceDetailsViewModel = private val viewModel: BluetoothDeviceDetailsViewModel =
ViewModelProvider( ViewModelProvider(
fragment, dashboardFragment,
BluetoothDeviceDetailsViewModel.Factory( BluetoothDeviceDetailsViewModel.Factory(
fragment.requireActivity().application, dashboardFragment.requireActivity().application,
bluetoothAdapter, bluetoothAdapter,
cachedDevice, cachedDevice,
backgroundCoroutineContext, backgroundCoroutineContext,
), ),
) )
.get(BluetoothDeviceDetailsViewModel::class.java) .get(BluetoothDeviceDetailsViewModel::class.java)
/** Updates bluetooth device details fragment layout. */ /** Updates bluetooth device details fragment layout. */
override fun updateLayout(fragmentType: FragmentTypeModel) { override fun updateLayout(fragmentType: FragmentTypeModel) {
fragment.setLoading(true, false) dashboardFragment.setLoading(true, false)
isLoading = true isLoading = true
fragment.lifecycleScope.launch { updateLayoutInternal(fragmentType) } dashboardFragment.lifecycleScope.launch { updateLayoutInternal(fragmentType) }
} }
private suspend fun updateLayoutInternal(fragmentType: FragmentTypeModel) { private suspend fun updateLayoutInternal(fragmentType: FragmentTypeModel) {
val items = viewModel.getItems(fragmentType) ?: run { val items =
fragment.setLoading(false, false) viewModel.getItems(fragmentType)
return ?: run {
} dashboardFragment.setLoading(false, false)
val layout = viewModel.getLayout(fragmentType) ?: run { return
fragment.setLoading(false, false) }
return
}
val prefKeyToSettingId = val prefKeyToSettingId =
items items
@@ -153,14 +134,14 @@ class DeviceDetailsFragmentFormatterImpl(
.associateBy({ it.preferenceKey }, { it.settingId }) .associateBy({ it.preferenceKey }, { it.settingId })
val settingIdToXmlPreferences: MutableMap<Int, Preference> = HashMap() val settingIdToXmlPreferences: MutableMap<Int, Preference> = HashMap()
for (i in 0 until fragment.preferenceScreen.preferenceCount) { for (i in 0 until dashboardFragment.preferenceScreen.preferenceCount) {
val pref = fragment.preferenceScreen.getPreference(i) val pref = dashboardFragment.preferenceScreen.getPreference(i)
prefKeyToSettingId[pref.key]?.let { id -> settingIdToXmlPreferences[id] = pref } prefKeyToSettingId[pref.key]?.let { id -> settingIdToXmlPreferences[id] = pref }
if (pref.key !in prefKeyToSettingId) { if (pref.key !in prefKeyToSettingId) {
getController(pref.key)?.let { disableController(it) } getController(pref.key)?.let { disableController(it) }
} }
} }
fragment.preferenceScreen.removeAll() dashboardFragment.preferenceScreen.removeAll()
for (job in prefVisibilityJobs) { for (job in prefVisibilityJobs) {
job.cancel() job.cancel()
} }
@@ -170,53 +151,83 @@ class DeviceDetailsFragmentFormatterImpl(
val settingId = settingItem.settingId val settingId = settingItem.settingId
if (settingIdToXmlPreferences.containsKey(settingId)) { if (settingIdToXmlPreferences.containsKey(settingId)) {
val pref = settingIdToXmlPreferences[settingId]!!.apply { order = row } val pref = settingIdToXmlPreferences[settingId]!!.apply { order = row }
fragment.preferenceScreen.addPreference(pref) dashboardFragment.preferenceScreen.addPreference(pref)
} else { } else {
val prefKey = getPreferenceKey(settingId) val prefKey = getPreferenceKey(settingId)
prefVisibilityJobs.add( prefVisibilityJobs.add(
getDevicesSettingForRow(layout, row) viewModel
.onEach { logItemShown(prefKey, it.isNotEmpty()) } .getDeviceSetting(cachedDevice, settingId)
.launchIn(fragment.lifecycleScope) .onEach { logItemShown(prefKey, it != null) }
.launchIn(dashboardFragment.lifecycleScope)
) )
val pref = if (settingId == DeviceSettingId.DEVICE_SETTING_ID_ANC) {
ComposePreference(context) // TODO(b/399316980): replace it with SegmentedButtonPreference once it's ready.
.apply { val pref =
key = prefKey ComposePreference(context)
order = row .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<Preference>(
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) } } .launchIn(dashboardFragment.lifecycleScope)
fragment.preferenceScreen.addPreference(pref) }
} }
} }
// 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) { for (row in items.indices) {
val settingItem = items[row] val settingItem = items[row]
val settingId = settingItem.settingId val settingId = settingItem.settingId
if (settingIdToXmlPreferences.containsKey(settingId)) { settingIdToXmlPreferences[settingId]?.let { pref ->
val pref = fragment.preferenceScreen.getPreference(row)
if (settingId == DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_PROFILES) { if (settingId == DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_PROFILES) {
(getController(pref.key) as? BluetoothDetailsProfilesController)?.run { (getController(pref.key) as? BluetoothDetailsProfilesController)?.run {
if (settingItem is DeviceSettingConfigItemModel.BuiltinItem.BluetoothProfilesItem) { if (
settingItem
is DeviceSettingConfigItemModel.BuiltinItem.BluetoothProfilesItem
) {
setInvisibleProfiles(settingItem.invisibleProfiles) setInvisibleProfiles(settingItem.invisibleProfiles)
setHasExtraSpace(false) setHasExtraSpace(false)
} }
} }
} }
getController(pref.key)?.displayPreference(fragment.preferenceScreen) getController(pref.key)?.displayPreference(dashboardFragment.preferenceScreen)
logItemShown(pref.key, pref.isVisible) logItemShown(pref.key, pref.isVisible)
} }
} }
fragment.lifecycleScope.launch { dashboardFragment.lifecycleScope.launch {
if (isLoading) { if (isLoading) {
fragment.setLoading(false, false) dashboardFragment.setLoading(false, false)
isLoading = false isLoading = false
} }
} }
@@ -236,87 +247,170 @@ class DeviceDetailsFragmentFormatterImpl(
} ?: emit(null) } ?: emit(null)
} }
private fun getDevicesSettingForRow( private fun buildPreference(
layout: DeviceSettingLayout, existedPref: Preference?,
row: Int, model: DeviceSettingPreferenceModel,
): Flow<List<DeviceSettingPreferenceModel>> = prefKey: String,
layout.rows[row].columns.flatMapLatest { columns -> highlighted: Boolean,
if (columns.isEmpty()) { ): Preference? =
flowOf(emptyList()) when (model) {
} else { is DeviceSettingPreferenceModel.PlainPreference -> {
combine( val pref =
columns.map { column -> existedPref
viewModel.getDeviceSetting(cachedDevice, column.settingId) ?: run {
} if (highlighted) SpotlightPreference(context) else Preference(context)
) { }
it.toList().filterNotNull() 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 getDrawable(deviceSettingIcon: DeviceSettingIcon?): Drawable? =
private fun buildPreference(layout: DeviceSettingLayout, row: Int, prefKey: String) { when (deviceSettingIcon) {
val contents by is DeviceSettingIcon.BitmapIcon ->
remember(row) { getDevicesSettingForRow(layout, row) } deviceSettingIcon.bitmap.toDrawable(context.resources)
.collectAsStateWithLifecycle(initialValue = listOf()) is DeviceSettingIcon.ResourceIcon -> context.getDrawable(deviceSettingIcon.resId)
null -> null
val highlighted by
remember(row) {
layout.rows[row].columns.map { columns -> columns.any { it.highlighted } }
} }
.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 val settings = contents
AnimatedVisibility(visible = settings.isNotEmpty(), enter = fadeIn(), exit = fadeOut()) { AnimatedVisibility(visible = settings != null, enter = fadeIn(), exit = fadeOut()) {
Box { (settings as? DeviceSettingPreferenceModel.MultiTogglePreference)?.let {
Box( buildMultiTogglePreference(it, prefKey)
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)
} }
} }
} }
@Composable
fun buildPreferences(settings: List<DeviceSettingPreferenceModel?>, 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 @Composable
private fun buildMultiTogglePreference( private fun buildMultiTogglePreference(
pref: DeviceSettingPreferenceModel.MultiTogglePreference, 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) { private fun logItemClick(preferenceKey: String, value: Int = 0) {
logAction(preferenceKey, SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_ITEM_CLICKED, value) logAction(preferenceKey, SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_ITEM_CLICKED, value)
} }
@@ -452,7 +445,7 @@ class DeviceDetailsFragmentFormatterImpl(
if (it) EVENT_VISIBLE else EVENT_INVISIBLE, if (it) EVENT_VISIBLE else EVENT_INVISIBLE,
) )
} }
.launchIn(fragment.lifecycleScope) .launchIn(dashboardFragment.lifecycleScope)
} }
} }
.value = visible .value = visible
@@ -485,7 +478,7 @@ class DeviceDetailsFragmentFormatterImpl(
private fun disableController(controller: AbstractPreferenceController) { private fun disableController(controller: AbstractPreferenceController) {
if (controller is LifecycleObserver) { if (controller is LifecycleObserver) {
fragment.settingsLifecycle.removeObserver(controller as LifecycleObserver) dashboardFragment.settingsLifecycle.removeObserver(controller as LifecycleObserver)
} }
if (controller is BlockingPrefWithSliceController) { if (controller is BlockingPrefWithSliceController) {
@@ -504,6 +497,19 @@ class DeviceDetailsFragmentFormatterImpl(
private fun getPreferenceKey(settingId: Int) = "DEVICE_SETTING_${settingId}" 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 { private companion object {
const val TAG = "DeviceDetailsFormatter" const val TAG = "DeviceDetailsFormatter"
const val EVENT_SWITCH_OFF = 0 const val EVENT_SWITCH_OFF = 0

View File

@@ -23,9 +23,6 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.android.settings.R 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.DeviceSettingPreferenceModel
import com.android.settings.bluetooth.ui.model.FragmentTypeModel import com.android.settings.bluetooth.ui.model.FragmentTypeModel
import com.android.settings.overlay.FeatureFactory.Companion.featureFactory import com.android.settings.overlay.FeatureFactory.Companion.featureFactory
@@ -39,11 +36,8 @@ import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
class BluetoothDeviceDetailsViewModel( class BluetoothDeviceDetailsViewModel(
private val application: Application, 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<DeviceSettingConfigItemModel.AppProvidedItem>()
.associateBy({ it.settingId }, { getDeviceSetting(cachedDevice, it.settingId) })
val configDeviceSetting =
configItems.map { idToDeviceSetting[it.settingId] ?: flowOf(null) }
val positionToSettingIds =
combine(configDeviceSetting) { settings ->
val positionMapping = mutableMapOf<Int, List<DeviceSettingLayoutColumn>>()
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( class Factory(
private val application: Application, private val application: Application,
private val bluetoothAdapter: BluetoothAdapter, private val bluetoothAdapter: BluetoothAdapter,

View File

@@ -16,7 +16,7 @@
package com.android.settings.bluetooth.ui.view package com.android.settings.bluetooth.ui.view
import android.app.settings.SettingsEnums; import android.app.settings.SettingsEnums
import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothAdapter
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@@ -25,6 +25,7 @@ import androidx.fragment.app.FragmentActivity
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import com.android.settings.bluetooth.ui.model.DeviceSettingPreferenceModel import com.android.settings.bluetooth.ui.model.DeviceSettingPreferenceModel
import com.android.settings.bluetooth.ui.model.FragmentTypeModel 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.CachedBluetoothDevice
import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId
import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository 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.DeviceSettingConfigItemModel
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigModel import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigModel
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingIcon 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.any
import org.mockito.Mockito.verify import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` import org.mockito.Mockito.`when`
import org.mockito.Spy
import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule import org.mockito.junit.MockitoRule
import org.mockito.kotlin.doNothing
import org.robolectric.Robolectric import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner import org.robolectric.RobolectricTestRunner
import org.robolectric.shadows.ShadowLooper import org.robolectric.shadows.ShadowLooper
import org.robolectric.shadows.ShadowLooper.shadowMainLooper import org.robolectric.shadows.ShadowLooper.shadowMainLooper
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
@RunWith(RobolectricTestRunner::class) @RunWith(RobolectricTestRunner::class)
class DeviceDetailsFragmentFormatterTest { class DeviceDetailsFragmentFormatterTest {
@@ -78,7 +81,7 @@ class DeviceDetailsFragmentFormatterTest {
@Mock private lateinit var headerController: AbstractPreferenceController @Mock private lateinit var headerController: AbstractPreferenceController
@Mock private lateinit var buttonController: 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 fragment: TestFragment
private lateinit var underTest: DeviceDetailsFragmentFormatter private lateinit var underTest: DeviceDetailsFragmentFormatter
private lateinit var featureFactory: FakeFeatureFactory private lateinit var featureFactory: FakeFeatureFactory
@@ -87,11 +90,15 @@ class DeviceDetailsFragmentFormatterTest {
@Before @Before
fun setUp() { fun setUp() {
context = ApplicationProvider.getApplicationContext()
featureFactory = FakeFeatureFactory.setupForTest() featureFactory = FakeFeatureFactory.setupForTest()
doNothing().`when`(context).startActivity(any(Intent::class.java))
`when`( `when`(
featureFactory.bluetoothFeatureProvider.getDeviceSettingRepository( featureFactory.bluetoothFeatureProvider.getDeviceSettingRepository(
eq(context), eq(bluetoothAdapter), any())) any(),
eq(bluetoothAdapter),
any(),
)
)
.thenReturn(repository) .thenReturn(repository)
fragmentActivity = Robolectric.setupActivity(FragmentActivity::class.java) fragmentActivity = Robolectric.setupActivity(FragmentActivity::class.java)
assertThat(fragmentActivity.applicationContext).isNotNull() assertThat(fragmentActivity.applicationContext).isNotNull()
@@ -115,7 +122,8 @@ class DeviceDetailsFragmentFormatterTest {
listOf(profileController, headerController, buttonController), listOf(profileController, headerController, buttonController),
bluetoothAdapter, bluetoothAdapter,
cachedDevice, cachedDevice,
testScope.testScheduler) testScope.testScheduler,
)
} }
@Test @Test
@@ -124,11 +132,16 @@ class DeviceDetailsFragmentFormatterTest {
`when`(repository.getDeviceSettingsConfig(cachedDevice)) `when`(repository.getDeviceSettingsConfig(cachedDevice))
.thenReturn( .thenReturn(
DeviceSettingConfigModel( DeviceSettingConfigModel(
listOf(), listOf(), DeviceSettingConfigItemModel.AppProvidedItem(12345, false))) listOf(),
val intent = Intent().apply { listOf(),
setAction(Intent.ACTION_VIEW) DeviceSettingConfigItemModel.AppProvidedItem(12345, false),
setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) )
} )
val intent =
Intent().apply {
setAction(Intent.ACTION_VIEW)
setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
`when`(repository.getDeviceSetting(cachedDevice, 12345)) `when`(repository.getDeviceSetting(cachedDevice, 12345))
.thenReturn( .thenReturn(
flowOf( flowOf(
@@ -136,12 +149,15 @@ class DeviceDetailsFragmentFormatterTest {
cachedDevice = cachedDevice, cachedDevice = cachedDevice,
id = 12345, id = 12345,
intent = intent, intent = intent,
))) )
)
)
var helpPreference: DeviceSettingPreferenceModel.HelpPreference? = null var helpPreference: DeviceSettingPreferenceModel.HelpPreference? = null
underTest.getMenuItem(FragmentTypeModel.DeviceDetailsMoreSettingsFragment).onEach { underTest
helpPreference = it .getMenuItem(FragmentTypeModel.DeviceDetailsMoreSettingsFragment)
}.launchIn(testScope.backgroundScope) .onEach { helpPreference = it }
.launchIn(testScope.backgroundScope)
delay(100) delay(100)
runCurrent() runCurrent()
ShadowLooper.idleMainLooper() ShadowLooper.idleMainLooper()
@@ -171,13 +187,19 @@ class DeviceDetailsFragmentFormatterTest {
listOf( listOf(
DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem( DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_HEADER, DeviceSettingId.DEVICE_SETTING_ID_HEADER,
highlighted = false, preferenceKey = "bluetooth_device_header"), highlighted = false,
preferenceKey = "bluetooth_device_header",
),
DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem( DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_PROFILES, DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_PROFILES,
highlighted = false, preferenceKey = "bluetooth_profiles"), highlighted = false,
preferenceKey = "bluetooth_profiles",
),
), ),
listOf(), listOf(),
null)) null,
)
)
underTest.updateLayout(FragmentTypeModel.DeviceDetailsMainFragment) underTest.updateLayout(FragmentTypeModel.DeviceDetailsMainFragment)
runCurrent() runCurrent()
@@ -189,13 +211,17 @@ class DeviceDetailsFragmentFormatterTest {
SettingsEnums.PAGE_UNKNOWN, SettingsEnums.PAGE_UNKNOWN,
SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_ITEM_SHOWN, SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_ITEM_SHOWN,
0, 0,
"bluetooth_device_header", 1) "bluetooth_device_header",
1,
)
verify(featureFactory.metricsFeatureProvider) verify(featureFactory.metricsFeatureProvider)
.action( .action(
SettingsEnums.PAGE_UNKNOWN, SettingsEnums.PAGE_UNKNOWN,
SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_ITEM_SHOWN, SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_ITEM_SHOWN,
0, 0,
"bluetooth_profiles", 1) "bluetooth_profiles",
1,
)
} }
} }
@@ -209,16 +235,22 @@ class DeviceDetailsFragmentFormatterTest {
DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem( DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_HEADER, DeviceSettingId.DEVICE_SETTING_ID_HEADER,
highlighted = false, highlighted = false,
preferenceKey = "bluetooth_device_header"), preferenceKey = "bluetooth_device_header",
),
DeviceSettingConfigItemModel.AppProvidedItem( DeviceSettingConfigItemModel.AppProvidedItem(
DeviceSettingId.DEVICE_SETTING_ID_ANC, highlighted = false), DeviceSettingId.DEVICE_SETTING_ID_ANC,
highlighted = false,
),
DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem( DeviceSettingConfigItemModel.BuiltinItem.CommonBuiltinItem(
DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_PROFILES, DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_PROFILES,
highlighted = false, highlighted = false,
preferenceKey = "bluetooth_profiles"), preferenceKey = "bluetooth_profiles",
),
), ),
listOf(), listOf(),
null)) null,
)
)
`when`(repository.getDeviceSetting(cachedDevice, DeviceSettingId.DEVICE_SETTING_ID_ANC)) `when`(repository.getDeviceSetting(cachedDevice, DeviceSettingId.DEVICE_SETTING_ID_ANC))
.thenReturn( .thenReturn(
flowOf( flowOf(
@@ -231,11 +263,17 @@ class DeviceDetailsFragmentFormatterTest {
ToggleModel( ToggleModel(
"", "",
DeviceSettingIcon.BitmapIcon( DeviceSettingIcon.BitmapIcon(
Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)))), Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
),
)
),
isActive = true, isActive = true,
state = DeviceSettingStateModel.MultiTogglePreferenceState(0), state = DeviceSettingStateModel.MultiTogglePreferenceState(0),
isAllowedChangingState = true, isAllowedChangingState = true,
updateState = {}))) updateState = {},
)
)
)
underTest.updateLayout(FragmentTypeModel.DeviceDetailsMainFragment) underTest.updateLayout(FragmentTypeModel.DeviceDetailsMainFragment)
runCurrent() runCurrent()
@@ -244,13 +282,119 @@ class DeviceDetailsFragmentFormatterTest {
.containsExactly( .containsExactly(
"bluetooth_device_header", "bluetooth_device_header",
"DEVICE_SETTING_${DeviceSettingId.DEVICE_SETTING_ID_ANC}", "DEVICE_SETTING_${DeviceSettingId.DEVICE_SETTING_ID_ANC}",
"bluetooth_profiles") "bluetooth_profiles",
)
verify(featureFactory.metricsFeatureProvider) verify(featureFactory.metricsFeatureProvider)
.action( .action(
SettingsEnums.PAGE_UNKNOWN, SettingsEnums.PAGE_UNKNOWN,
SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_ITEM_SHOWN, SettingsEnums.ACTION_BLUETOOTH_DEVICE_DETAILS_ITEM_SHOWN,
0, 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,
) )
} }
} }

View File

@@ -20,7 +20,6 @@ import android.app.Application
import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothAdapter
import android.graphics.Bitmap import android.graphics.Bitmap
import androidx.test.core.app.ApplicationProvider 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.DeviceSettingPreferenceModel
import com.android.settings.bluetooth.ui.model.FragmentTypeModel import com.android.settings.bluetooth.ui.model.FragmentTypeModel
import com.android.settings.testutils.FakeFeatureFactory 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<List<Int>> {
val latestLayout = MutableList(layout.rows.size) { emptyList<Int>() }
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) = private fun buildMultiTogglePreference(settingId: Int) =
DeviceSettingModel.MultiTogglePreference( DeviceSettingModel.MultiTogglePreference(
cachedDevice, cachedDevice,