Reimplement device details UI without ComposePreference
Replace ComposePreference with androidx.preference.Preference. ANC toggle will still use Compose until SegmentedButtonPreference is ready. BUG: 402036473 Test: local tested Flag: com.android.settings.flags.enable_bluetooth_device_details_polish Change-Id: I5114af8f2d679d695b3c5ef4d7be2874245c435e
This commit is contained in:
@@ -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>
|
73
res/layout/bluetooth_device_spotlight_preference.xml
Normal file
73
res/layout/bluetooth_device_spotlight_preference.xml
Normal 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>
|
@@ -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)
|
@@ -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<AbstractPreferenceController>,
|
||||
private val bluetoothAdapter: BluetoothAdapter,
|
||||
private val cachedDevice: CachedBluetoothDevice,
|
||||
@@ -120,9 +103,9 @@ class DeviceDetailsFragmentFormatterImpl(
|
||||
|
||||
private val viewModel: BluetoothDeviceDetailsViewModel =
|
||||
ViewModelProvider(
|
||||
fragment,
|
||||
dashboardFragment,
|
||||
BluetoothDeviceDetailsViewModel.Factory(
|
||||
fragment.requireActivity().application,
|
||||
dashboardFragment.requireActivity().application,
|
||||
bluetoothAdapter,
|
||||
cachedDevice,
|
||||
backgroundCoroutineContext,
|
||||
@@ -132,18 +115,16 @@ class DeviceDetailsFragmentFormatterImpl(
|
||||
|
||||
/** 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)
|
||||
val items =
|
||||
viewModel.getItems(fragmentType)
|
||||
?: run {
|
||||
dashboardFragment.setLoading(false, false)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -153,14 +134,14 @@ class DeviceDetailsFragmentFormatterImpl(
|
||||
.associateBy({ it.preferenceKey }, { it.settingId })
|
||||
|
||||
val settingIdToXmlPreferences: MutableMap<Int, Preference> = 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)
|
||||
)
|
||||
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 { buildPreference(layout, row, prefKey) } }
|
||||
fragment.preferenceScreen.addPreference(pref)
|
||||
.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) }
|
||||
}
|
||||
.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,84 +247,167 @@ class DeviceDetailsFragmentFormatterImpl(
|
||||
} ?: emit(null)
|
||||
}
|
||||
|
||||
private fun getDevicesSettingForRow(
|
||||
layout: DeviceSettingLayout,
|
||||
row: Int,
|
||||
): Flow<List<DeviceSettingPreferenceModel>> =
|
||||
layout.rows[row].columns.flatMapLatest { columns ->
|
||||
if (columns.isEmpty()) {
|
||||
flowOf(emptyList())
|
||||
} else {
|
||||
combine(
|
||||
columns.map { column ->
|
||||
viewModel.getDeviceSetting(cachedDevice, column.settingId)
|
||||
}
|
||||
) {
|
||||
it.toList().filterNotNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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 } }
|
||||
}
|
||||
.collectAsStateWithLifecycle(initialValue = false)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun buildPreferences(settings: List<DeviceSettingPreferenceModel?>, prefKey: String) {
|
||||
when (settings.size) {
|
||||
0 -> {}
|
||||
1 -> {
|
||||
when (val setting = settings[0]) {
|
||||
private fun buildPreference(
|
||||
existedPref: Preference?,
|
||||
model: DeviceSettingPreferenceModel,
|
||||
prefKey: String,
|
||||
highlighted: Boolean,
|
||||
): Preference? =
|
||||
when (model) {
|
||||
is DeviceSettingPreferenceModel.PlainPreference -> {
|
||||
buildPlainPreference(setting, prefKey)
|
||||
val pref =
|
||||
existedPref
|
||||
?: run {
|
||||
if (highlighted) SpotlightPreference(context) else Preference(context)
|
||||
}
|
||||
is DeviceSettingPreferenceModel.SwitchPreference -> {
|
||||
buildSwitchPreference(setting, prefKey)
|
||||
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 -> {
|
||||
buildMultiTogglePreference(setting, prefKey)
|
||||
// TODO(b/399316980): implemented it by SegmentedButtonPreference
|
||||
null
|
||||
}
|
||||
is DeviceSettingPreferenceModel.FooterPreference -> {
|
||||
buildFooterPreference(setting)
|
||||
val pref = existedPref as? FooterPreference ?: FooterPreference(context)
|
||||
pref.apply { title = model.footerText }
|
||||
}
|
||||
is DeviceSettingPreferenceModel.MoreSettingsPreference -> {
|
||||
buildMoreSettingsPreference(prefKey)
|
||||
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)
|
||||
}
|
||||
is DeviceSettingPreferenceModel.HelpPreference -> {}
|
||||
null -> {}
|
||||
)
|
||||
.launch()
|
||||
return true
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
is DeviceSettingPreferenceModel.HelpPreference -> {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@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 != null, enter = fadeIn(), exit = fadeOut()) {
|
||||
(settings as? DeviceSettingPreferenceModel.MultiTogglePreference)?.let {
|
||||
buildMultiTogglePreference(it, prefKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
@@ -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<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(
|
||||
private val application: Application,
|
||||
private val bluetoothAdapter: BluetoothAdapter,
|
||||
|
@@ -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,8 +132,13 @@ class DeviceDetailsFragmentFormatterTest {
|
||||
`when`(repository.getDeviceSettingsConfig(cachedDevice))
|
||||
.thenReturn(
|
||||
DeviceSettingConfigModel(
|
||||
listOf(), listOf(), DeviceSettingConfigItemModel.AppProvidedItem(12345, false)))
|
||||
val intent = Intent().apply {
|
||||
listOf(),
|
||||
listOf(),
|
||||
DeviceSettingConfigItemModel.AppProvidedItem(12345, false),
|
||||
)
|
||||
)
|
||||
val intent =
|
||||
Intent().apply {
|
||||
setAction(Intent.ACTION_VIEW)
|
||||
setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -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<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) =
|
||||
DeviceSettingModel.MultiTogglePreference(
|
||||
cachedDevice,
|
||||
|
Reference in New Issue
Block a user