Merge "Rearrange bluetooth device details fragment according to config" into main
diff --git a/src/com/android/settings/bluetooth/BlockingPrefWithSliceController.java b/src/com/android/settings/bluetooth/BlockingPrefWithSliceController.java
index 0690186..442acd2 100644
--- a/src/com/android/settings/bluetooth/BlockingPrefWithSliceController.java
+++ b/src/com/android/settings/bluetooth/BlockingPrefWithSliceController.java
@@ -101,7 +101,8 @@
return mUri != null ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
}
- public void setSliceUri(Uri uri) {
+ /** Sets Slice uri for the preference. */
+ public void setSliceUri(@Nullable Uri uri) {
mUri = uri;
mLiveData = SliceLiveData.fromUri(mContext, mUri, (int type, Throwable source) -> {
Log.w(TAG, "Slice may be null. uri = " + uri + ", error = " + type);
diff --git a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java
index ccf38ed..bd762a1 100644
--- a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java
+++ b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java
@@ -43,10 +43,12 @@
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.settings.R;
+import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatter;
import com.android.settings.connecteddevice.stylus.StylusDevicesController;
import com.android.settings.core.SettingsUIDeviceConfig;
import com.android.settings.dashboard.RestrictedDashboardFragment;
@@ -60,9 +62,11 @@
import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import com.android.settingslib.core.lifecycle.Lifecycle;
+import com.android.settingslib.core.lifecycle.LifecycleObserver;
import java.util.ArrayList;
import java.util.List;
+import java.util.function.Consumer;
public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment {
public static final String KEY_DEVICE_ADDRESS = "device_address";
@@ -98,6 +102,8 @@
@VisibleForTesting
CachedBluetoothDevice mCachedDevice;
BluetoothAdapter mBluetoothAdapter;
+ @VisibleForTesting
+ DeviceDetailsFragmentFormatter mFormatter;
@Nullable
InputDevice mInputDevice;
@@ -214,18 +220,29 @@
finish();
return;
}
- use(AdvancedBluetoothDetailsHeaderController.class).init(mCachedDevice, this);
- use(LeAudioBluetoothDetailsHeaderController.class).init(mCachedDevice, mManager, this);
- use(KeyboardSettingsPreferenceController.class).init(mCachedDevice);
+ getController(
+ AdvancedBluetoothDetailsHeaderController.class,
+ controller -> controller.init(mCachedDevice, this));
+ getController(
+ LeAudioBluetoothDetailsHeaderController.class,
+ controller -> controller.init(mCachedDevice, mManager, this));
+ getController(
+ KeyboardSettingsPreferenceController.class,
+ controller -> controller.init(mCachedDevice));
final BluetoothFeatureProvider featureProvider =
FeatureFactory.getFeatureFactory().getBluetoothFeatureProvider();
final boolean sliceEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI,
SettingsUIDeviceConfig.BT_SLICE_SETTINGS_ENABLED, true);
- use(BlockingPrefWithSliceController.class).setSliceUri(sliceEnabled
- ? featureProvider.getBluetoothDeviceSettingsUri(mCachedDevice.getDevice())
- : null);
+ getController(
+ BlockingPrefWithSliceController.class,
+ controller ->
+ controller.setSliceUri(
+ sliceEnabled
+ ? featureProvider.getBluetoothDeviceSettingsUri(
+ mCachedDevice.getDevice())
+ : null));
mManager.getEventManager().registerCallback(mBluetoothCallback);
mBluetoothAdapter.addOnMetadataChangedListener(
@@ -257,21 +274,35 @@
}
}
mExtraControlUriLoaded |= controlUri != null;
- final SlicePreferenceController slicePreferenceController = use(
- SlicePreferenceController.class);
- slicePreferenceController.setSliceUri(sliceEnabled ? controlUri : null);
- slicePreferenceController.onStart();
- slicePreferenceController.displayPreference(getPreferenceScreen());
+
+ Uri finalControlUri = controlUri;
+ getController(SlicePreferenceController.class, controller -> {
+ controller.setSliceUri(sliceEnabled ? finalControlUri : null);
+ controller.onStart();
+ controller.displayPreference(getPreferenceScreen());
+ });
+
// Temporarily fix the issue that the page will be automatically scrolled to a wrong
// position when entering the page. This will make sure the bluetooth header is shown on top
// of the page.
- use(LeAudioBluetoothDetailsHeaderController.class).displayPreference(
- getPreferenceScreen());
- use(AdvancedBluetoothDetailsHeaderController.class).displayPreference(
- getPreferenceScreen());
- use(BluetoothDetailsHeaderController.class).displayPreference(
- getPreferenceScreen());
+ getController(
+ LeAudioBluetoothDetailsHeaderController.class,
+ controller -> controller.displayPreference(getPreferenceScreen()));
+ getController(
+ AdvancedBluetoothDetailsHeaderController.class,
+ controller -> controller.displayPreference(getPreferenceScreen()));
+ getController(
+ BluetoothDetailsHeaderController.class,
+ controller -> controller.displayPreference(getPreferenceScreen()));
+ }
+
+ protected <T extends AbstractPreferenceController> void getController(Class<T> clazz,
+ Consumer<T> action) {
+ T controller = use(clazz);
+ if (controller != null) {
+ action.accept(controller);
+ }
}
private final ViewTreeObserver.OnGlobalLayoutListener mOnGlobalLayoutListener =
@@ -309,6 +340,14 @@
}
@Override
+ public void onCreatePreferences(@NonNull Bundle savedInstanceState, @NonNull String rootKey) {
+ super.onCreatePreferences(savedInstanceState, rootKey);
+ if (Flags.enableBluetoothDeviceDetailsPolish()) {
+ mFormatter.updateLayout();
+ }
+ }
+
+ @Override
public void onResume() {
super.onResume();
finishFragmentIfNecessary();
@@ -359,7 +398,29 @@
}
@Override
+ protected void addPreferenceController(AbstractPreferenceController controller) {
+ if (Flags.enableBluetoothDeviceDetailsPolish()) {
+ List<String> keys = mFormatter.getVisiblePreferenceKeysForMainPage();
+ Lifecycle lifecycle = getSettingsLifecycle();
+ if (keys == null || keys.contains(controller.getPreferenceKey())) {
+ super.addPreferenceController(controller);
+ } else if (controller instanceof LifecycleObserver) {
+ lifecycle.removeObserver((LifecycleObserver) controller);
+ }
+ } else {
+ super.addPreferenceController(controller);
+ }
+ }
+
+ @Override
protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
+ if (Flags.enableBluetoothDeviceDetailsPolish()) {
+ mFormatter =
+ FeatureFactory.getFeatureFactory()
+ .getBluetoothFeatureProvider()
+ .getDeviceDetailsFragmentFormatter(
+ requireContext(), this, mBluetoothAdapter, mCachedDevice);
+ }
ArrayList<AbstractPreferenceController> controllers = new ArrayList<>();
if (mCachedDevice != null) {
diff --git a/src/com/android/settings/bluetooth/BluetoothFeatureProvider.java b/src/com/android/settings/bluetooth/BluetoothFeatureProvider.java
index 1751082..5941344 100644
--- a/src/com/android/settings/bluetooth/BluetoothFeatureProvider.java
+++ b/src/com/android/settings/bluetooth/BluetoothFeatureProvider.java
@@ -16,15 +16,21 @@
package com.android.settings.bluetooth;
+import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.ComponentName;
import android.content.Context;
import android.media.Spatializer;
import android.net.Uri;
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LifecycleCoroutineScope;
import androidx.preference.Preference;
+import com.android.settings.SettingsPreferenceFragment;
+import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatter;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository;
import java.util.List;
import java.util.Set;
@@ -84,4 +90,19 @@
*/
Set<String> getInvisibleProfilePreferenceKeys(
Context context, BluetoothDevice bluetoothDevice);
+
+ /** Gets DeviceSettingRepository. */
+ @NonNull
+ DeviceSettingRepository getDeviceSettingRepository(
+ @NonNull Context context,
+ @NonNull BluetoothAdapter bluetoothAdapter,
+ @NonNull LifecycleCoroutineScope scope);
+
+ /** Gets device details fragment layout formatter. */
+ @NonNull
+ DeviceDetailsFragmentFormatter getDeviceDetailsFragmentFormatter(
+ @NonNull Context context,
+ @NonNull SettingsPreferenceFragment fragment,
+ @NonNull BluetoothAdapter bluetoothAdapter,
+ @NonNull CachedBluetoothDevice cachedDevice);
}
diff --git a/src/com/android/settings/bluetooth/BluetoothFeatureProviderImpl.java b/src/com/android/settings/bluetooth/BluetoothFeatureProviderImpl.java
index 2d4ac49..ae6e740 100644
--- a/src/com/android/settings/bluetooth/BluetoothFeatureProviderImpl.java
+++ b/src/com/android/settings/bluetooth/BluetoothFeatureProviderImpl.java
@@ -16,6 +16,9 @@
package com.android.settings.bluetooth;
+import static com.android.settings.bluetooth.utils.DeviceSettingUtilsKt.createDeviceSettingRepository;
+
+import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.ComponentName;
import android.content.Context;
@@ -23,10 +26,16 @@
import android.media.Spatializer;
import android.net.Uri;
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LifecycleCoroutineScope;
import androidx.preference.Preference;
+import com.android.settings.SettingsPreferenceFragment;
+import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatter;
+import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatterImpl;
import com.android.settingslib.bluetooth.BluetoothUtils;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
@@ -73,4 +82,24 @@
Context context, BluetoothDevice bluetoothDevice) {
return ImmutableSet.of();
}
+
+ @Override
+ @NonNull
+ public DeviceSettingRepository getDeviceSettingRepository(
+ @NonNull Context context,
+ @NonNull BluetoothAdapter bluetoothAdapter,
+ @NonNull LifecycleCoroutineScope scope) {
+ return createDeviceSettingRepository(context, bluetoothAdapter, scope);
+ }
+
+ @Override
+ @NonNull
+ public DeviceDetailsFragmentFormatter getDeviceDetailsFragmentFormatter(
+ @NonNull Context context,
+ @NonNull SettingsPreferenceFragment fragment,
+ @NonNull BluetoothAdapter bluetoothAdapter,
+ @NonNull CachedBluetoothDevice cachedDevice) {
+ return new DeviceDetailsFragmentFormatterImpl(
+ context, fragment, bluetoothAdapter, cachedDevice);
+ }
}
diff --git a/src/com/android/settings/bluetooth/ui/MultiTogglePreferenceGroup.kt b/src/com/android/settings/bluetooth/ui/composable/MultiTogglePreferenceGroup.kt
similarity index 99%
rename from src/com/android/settings/bluetooth/ui/MultiTogglePreferenceGroup.kt
rename to src/com/android/settings/bluetooth/ui/composable/MultiTogglePreferenceGroup.kt
index e4ca00d..b42e7d0 100644
--- a/src/com/android/settings/bluetooth/ui/MultiTogglePreferenceGroup.kt
+++ b/src/com/android/settings/bluetooth/ui/composable/MultiTogglePreferenceGroup.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.settings.bluetooth.ui
+package com.android.settings.bluetooth.ui.composable
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
diff --git a/src/com/android/settings/bluetooth/ui/layout/DeviceSettingLayout.kt b/src/com/android/settings/bluetooth/ui/layout/DeviceSettingLayout.kt
new file mode 100644
index 0000000..87e2e8b
--- /dev/null
+++ b/src/com/android/settings/bluetooth/ui/layout/DeviceSettingLayout.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.bluetooth.ui.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 settingIds: Flow<List<Int>>)
diff --git a/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt b/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt
new file mode 100644
index 0000000..3b77aae
--- /dev/null
+++ b/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatter.kt
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.bluetooth.ui.view
+
+import android.bluetooth.BluetoothAdapter
+import android.content.Context
+import android.util.Log
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.Icon
+import androidx.compose.material3.LocalContentColor
+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.asImageBitmap
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.preference.Preference
+import com.android.settings.SettingsPreferenceFragment
+import com.android.settings.bluetooth.ui.composable.MultiTogglePreferenceGroup
+import com.android.settings.bluetooth.ui.layout.DeviceSettingLayout
+import com.android.settings.bluetooth.ui.viewmodel.BluetoothDeviceDetailsViewModel
+import com.android.settings.overlay.FeatureFactory.Companion.featureFactory
+import com.android.settings.spa.preference.ComposePreference
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel
+import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel
+import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel
+import com.android.settingslib.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 kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.runBlocking
+
+/** Handles device details fragment layout according to config. */
+interface DeviceDetailsFragmentFormatter {
+ /** Gets keys of visible preferences in built-in preference in xml. */
+ fun getVisiblePreferenceKeysForMainPage(): List<String>?
+
+ /** Updates device details fragment layout. */
+ fun updateLayout()
+}
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class DeviceDetailsFragmentFormatterImpl(
+ private val context: Context,
+ private val fragment: SettingsPreferenceFragment,
+ bluetoothAdapter: BluetoothAdapter,
+ private val cachedDevice: CachedBluetoothDevice
+) : DeviceDetailsFragmentFormatter {
+ private val repository =
+ featureFactory.bluetoothFeatureProvider.getDeviceSettingRepository(
+ context, bluetoothAdapter, fragment.lifecycleScope)
+ private val viewModel: BluetoothDeviceDetailsViewModel =
+ ViewModelProvider(
+ fragment,
+ BluetoothDeviceDetailsViewModel.Factory(
+ repository,
+ cachedDevice,
+ ))
+ .get(BluetoothDeviceDetailsViewModel::class.java)
+
+ override fun getVisiblePreferenceKeysForMainPage(): List<String>? = runBlocking {
+ viewModel.getItems()?.filterIsInstance<DeviceSettingConfigItemModel.BuiltinItem>()?.map {
+ it.preferenceKey
+ }
+ }
+
+ /** Updates bluetooth device details fragment layout. */
+ override fun updateLayout() = runBlocking {
+ val items = viewModel.getItems() ?: return@runBlocking
+ val layout = viewModel.getLayout() ?: return@runBlocking
+ val prefKeyToSettingId =
+ items
+ .filterIsInstance<DeviceSettingConfigItemModel.BuiltinItem>()
+ .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)
+ prefKeyToSettingId[pref.key]?.let { id -> settingIdToXmlPreferences[id] = pref }
+ }
+ fragment.preferenceScreen.removeAll()
+
+ for (row in items.indices) {
+ val settingId = items[row].settingId
+ if (settingIdToXmlPreferences.containsKey(settingId)) {
+ fragment.preferenceScreen.addPreference(
+ settingIdToXmlPreferences[settingId]!!.apply { order = row })
+ } else {
+ val pref =
+ ComposePreference(context)
+ .apply {
+ key = getPreferenceKey(settingId)
+ order = row
+ }
+ .also { pref -> pref.setContent { buildPreference(layout, row) } }
+ fragment.preferenceScreen.addPreference(pref)
+ }
+ }
+ }
+
+ @Composable
+ private fun buildPreference(layout: DeviceSettingLayout, row: Int) {
+ val contents by
+ remember(row) {
+ layout.rows[row].settingIds.flatMapLatest { settingIds ->
+ if (settingIds.isEmpty()) {
+ flowOf(emptyList<DeviceSettingModel>())
+ } else {
+ combine(
+ settingIds.map { settingId ->
+ viewModel.getDeviceSetting(cachedDevice, settingId)
+ }) {
+ it.toList()
+ }
+ }
+ }
+ }
+ .collectAsStateWithLifecycle(initialValue = listOf())
+
+ val settings = contents
+ when (settings.size) {
+ 0 -> {}
+ 1 -> {
+ when (val setting = settings[0]) {
+ is DeviceSettingModel.ActionSwitchPreference -> {
+ buildActionSwitchPreference(setting)
+ }
+ is DeviceSettingModel.MultiTogglePreference -> {
+ buildMultiTogglePreference(listOf(setting))
+ }
+ null -> {}
+ else -> {
+ Log.w(TAG, "Unknown preference type ${setting.id}, skip.")
+ }
+ }
+ }
+ else -> {
+ if (!settings.all { it is DeviceSettingModel.MultiTogglePreference }) {
+ return
+ }
+ buildMultiTogglePreference(
+ settings.filterIsInstance<DeviceSettingModel.MultiTogglePreference>())
+ }
+ }
+ }
+
+ @Composable
+ private fun buildMultiTogglePreference(prefs: List<DeviceSettingModel.MultiTogglePreference>) {
+ MultiTogglePreferenceGroup(prefs)
+ }
+
+ @Composable
+ private fun buildActionSwitchPreference(model: DeviceSettingModel.ActionSwitchPreference) {
+ if (model.switchState != null) {
+ val switchPrefModel =
+ object : SwitchPreferenceModel {
+ override val title = model.title
+ override val summary = { model.summary ?: "" }
+ override val checked = { model.switchState?.checked }
+ override val onCheckedChange = { newChecked: Boolean ->
+ model.updateState?.invoke(
+ DeviceSettingStateModel.ActionSwitchPreferenceState(newChecked))
+ Unit
+ }
+ override val icon = @Composable { deviceSettingIcon(model) }
+ }
+ if (model.intent != null) {
+ TwoTargetSwitchPreference(switchPrefModel) { context.startActivity(model.intent) }
+ } else {
+ SwitchPreference(switchPrefModel)
+ }
+ } else {
+ SpaPreference(
+ object : PreferenceModel {
+ override val title = model.title
+ override val summary = { model.summary ?: "" }
+ override val onClick = {
+ model.intent?.let { context.startActivity(it) }
+ Unit
+ }
+ override val icon = @Composable { deviceSettingIcon(model) }
+ })
+ }
+ }
+
+ @Composable
+ private fun deviceSettingIcon(model: DeviceSettingModel.ActionSwitchPreference) {
+ model.icon?.let { bitmap ->
+ Icon(
+ bitmap.asImageBitmap(),
+ contentDescription = null,
+ modifier = Modifier.size(SettingsDimension.itemIconSize),
+ tint = LocalContentColor.current)
+ }
+ }
+
+ private fun getPreferenceKey(settingId: Int) = "DEVICE_SETTING_${settingId}"
+
+ companion object {
+ const val TAG = "DeviceDetailsFormatter"
+ }
+}
diff --git a/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModel.kt b/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModel.kt
new file mode 100644
index 0000000..1c48614
--- /dev/null
+++ b/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModel.kt
@@ -0,0 +1,107 @@
+/*
+ * 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.viewmodel
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewModelScope
+import com.android.settings.bluetooth.ui.layout.DeviceSettingLayout
+import com.android.settings.bluetooth.ui.layout.DeviceSettingLayoutRow
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId
+import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository
+import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigItemModel
+import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+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 deviceSettingRepository: DeviceSettingRepository,
+ private val cachedDevice: CachedBluetoothDevice,
+) : ViewModel() {
+ private val items =
+ viewModelScope.async(Dispatchers.IO, start = CoroutineStart.LAZY) {
+ deviceSettingRepository.getDeviceSettingsConfig(cachedDevice)
+ }
+
+ suspend fun getItems(): List<DeviceSettingConfigItemModel>? = items.await()?.mainItems
+
+ fun getDeviceSetting(cachedDevice: CachedBluetoothDevice, @DeviceSettingId settingId: Int) =
+ deviceSettingRepository.getDeviceSetting(cachedDevice, settingId)
+
+ suspend fun getLayout(): DeviceSettingLayout? {
+ val configItems = getItems() ?: 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<Int>>()
+ var multiToggleSettingIds: MutableList<Int>? = null
+ for (i in settings.indices) {
+ val configItem = configItems[i]
+ val setting = settings[i]
+ val isXmlPreference = configItem is DeviceSettingConfigItemModel.BuiltinItem
+ if (!isXmlPreference && setting == null) {
+ continue
+ }
+ if (setting !is DeviceSettingModel.MultiTogglePreference) {
+ multiToggleSettingIds = null
+ positionMapping[i] = listOf(configItem.settingId)
+ continue
+ }
+
+ if (multiToggleSettingIds != null) {
+ multiToggleSettingIds.add(setting.id)
+ } else {
+ multiToggleSettingIds = mutableListOf(setting.id)
+ positionMapping[i] = multiToggleSettingIds
+ }
+ }
+ positionMapping
+ }
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), initialValue = mapOf())
+ return DeviceSettingLayout(
+ configItems.indices.map { idx ->
+ DeviceSettingLayoutRow(positionToSettingIds.map { it[idx] ?: emptyList() })
+ })
+ }
+
+ class Factory(
+ private val deviceSettingRepository: DeviceSettingRepository,
+ private val cachedDevice: CachedBluetoothDevice,
+ ) : ViewModelProvider.Factory {
+ override fun <T : ViewModel> create(modelClass: Class<T>): T {
+ @Suppress("UNCHECKED_CAST")
+ return BluetoothDeviceDetailsViewModel(deviceSettingRepository, cachedDevice) as T
+ }
+ }
+
+ companion object {
+ private const val TAG = "BluetoothDeviceDetailsViewModel"
+ }
+}
diff --git a/src/com/android/settings/bluetooth/utils/DeviceSettingUtils.kt b/src/com/android/settings/bluetooth/utils/DeviceSettingUtils.kt
new file mode 100644
index 0000000..1bb8f20
--- /dev/null
+++ b/src/com/android/settings/bluetooth/utils/DeviceSettingUtils.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.utils
+
+import android.bluetooth.BluetoothAdapter
+import android.content.Context
+import androidx.lifecycle.LifecycleCoroutineScope
+import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepositoryImpl
+import kotlinx.coroutines.Dispatchers
+
+fun createDeviceSettingRepository(
+ context: Context,
+ bluetoothAdapter: BluetoothAdapter,
+ coroutineScope: LifecycleCoroutineScope
+) = DeviceSettingRepositoryImpl(context, bluetoothAdapter, coroutineScope, Dispatchers.IO)
diff --git a/src/com/android/settings/slices/SlicePreferenceController.java b/src/com/android/settings/slices/SlicePreferenceController.java
index 5e8fb26..2e835a0 100644
--- a/src/com/android/settings/slices/SlicePreferenceController.java
+++ b/src/com/android/settings/slices/SlicePreferenceController.java
@@ -20,6 +20,7 @@
import android.net.Uri;
import android.util.Log;
+import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
@@ -61,7 +62,8 @@
return mUri != null ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
}
- public void setSliceUri(Uri uri) {
+ /** Sets Slice uri for the preference. */
+ public void setSliceUri(@Nullable Uri uri) {
mUri = uri;
mLiveData = SliceLiveData.fromUri(mContext, mUri, (int type, Throwable source) -> {
Log.w(TAG, "Slice may be null. uri = " + uri + ", error = " + type);
diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragmentTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragmentTest.java
index 50aa771..19d0edd 100644
--- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragmentTest.java
+++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragmentTest.java
@@ -50,6 +50,7 @@
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
+import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatter;
import com.android.settings.testutils.FakeFeatureFactory;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
@@ -101,6 +102,8 @@
private InputManager mInputManager;
@Mock
private CompanionDeviceManager mCompanionDeviceManager;
+ @Mock
+ private DeviceDetailsFragmentFormatter mFormatter;
@Before
public void setUp() {
@@ -111,7 +114,10 @@
.getSystemService(CompanionDeviceManager.class);
when(mCompanionDeviceManager.getAllAssociations()).thenReturn(ImmutableList.of());
removeInputDeviceWithMatchingBluetoothAddress();
- FakeFeatureFactory.setupForTest();
+ FakeFeatureFactory fakeFeatureFactory = FakeFeatureFactory.setupForTest();
+ when(fakeFeatureFactory.mBluetoothFeatureProvider.getDeviceDetailsFragmentFormatter(any(),
+ any(), any(), eq(mCachedDevice))).thenReturn(mFormatter);
+ when(mFormatter.getVisiblePreferenceKeysForMainPage()).thenReturn(null);
mFragment = setupFragment();
mFragment.onAttach(mContext);
diff --git a/tests/robotests/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatterTest.kt b/tests/robotests/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatterTest.kt
new file mode 100644
index 0000000..468a2f0
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/bluetooth/ui/view/DeviceDetailsFragmentFormatterTest.kt
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.bluetooth.ui.view
+
+import android.bluetooth.BluetoothAdapter
+import android.content.Context
+import android.graphics.Bitmap
+import androidx.fragment.app.FragmentActivity
+import androidx.preference.Preference
+import androidx.preference.PreferenceManager
+import androidx.preference.PreferenceScreen
+import androidx.test.core.app.ApplicationProvider
+import com.android.settings.dashboard.DashboardFragment
+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.DeviceSettingConfigItemModel
+import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigModel
+import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel
+import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel
+import com.android.settingslib.bluetooth.devicesettings.shared.model.ToggleModel
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.robolectric.Robolectric
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.shadows.ShadowLooper.shadowMainLooper
+
+@RunWith(RobolectricTestRunner::class)
+class DeviceDetailsFragmentFormatterTest {
+ @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+ @Mock private lateinit var cachedDevice: CachedBluetoothDevice
+ @Mock private lateinit var bluetoothAdapter: BluetoothAdapter
+ @Mock private lateinit var repository: DeviceSettingRepository
+
+ private lateinit var fragment: TestFragment
+ private lateinit var underTest: DeviceDetailsFragmentFormatter
+ private lateinit var featureFactory: FakeFeatureFactory
+ private val testScope = TestScope()
+
+ @Before
+ fun setUp() {
+ val context = ApplicationProvider.getApplicationContext<Context>()
+ featureFactory = FakeFeatureFactory.setupForTest()
+ `when`(
+ featureFactory.bluetoothFeatureProvider.getDeviceSettingRepository(
+ eq(context), eq(bluetoothAdapter), any()))
+ .thenReturn(repository)
+ val fragmentActivity = Robolectric.setupActivity(FragmentActivity::class.java)
+ assertThat(fragmentActivity.applicationContext).isNotNull()
+ fragment = TestFragment(context)
+ fragmentActivity.supportFragmentManager.beginTransaction().add(fragment, null).commit()
+ shadowMainLooper().idle()
+
+ fragment.preferenceScreen.run {
+ addPreference(Preference(context).apply { key = "bluetooth_device_header" })
+ addPreference(Preference(context).apply { key = "action_buttons" })
+ addPreference(Preference(context).apply { key = "keyboard_settings" })
+ }
+
+ underTest =
+ DeviceDetailsFragmentFormatterImpl(context, fragment, bluetoothAdapter, cachedDevice)
+ }
+
+ @Test
+ fun getVisiblePreferenceKeysForMainPage_hasConfig_returnList() {
+ testScope.runTest {
+ `when`(repository.getDeviceSettingsConfig(cachedDevice))
+ .thenReturn(
+ DeviceSettingConfigModel(
+ listOf(
+ DeviceSettingConfigItemModel.BuiltinItem(
+ DeviceSettingId.DEVICE_SETTING_ID_HEADER,
+ "bluetooth_device_header"),
+ DeviceSettingConfigItemModel.BuiltinItem(
+ DeviceSettingId.DEVICE_SETTING_ID_ACTION_BUTTONS, "action_buttons"),
+ ),
+ listOf(),
+ "footer"))
+
+ val keys = underTest.getVisiblePreferenceKeysForMainPage()
+
+ assertThat(keys).containsExactly("bluetooth_device_header", "action_buttons")
+ }
+ }
+
+ @Test
+ fun getVisiblePreferenceKeysForMainPage_noConfig_returnNull() {
+ testScope.runTest {
+ `when`(repository.getDeviceSettingsConfig(cachedDevice)).thenReturn(null)
+
+ val keys = underTest.getVisiblePreferenceKeysForMainPage()
+
+ assertThat(keys).isNull()
+ }
+ }
+
+ @Test
+ fun updateLayout_configIsNull_notChange() {
+ testScope.runTest {
+ `when`(repository.getDeviceSettingsConfig(cachedDevice)).thenReturn(null)
+
+ underTest.updateLayout()
+
+ assertThat(getDisplayedPreferences().map { it.key })
+ .containsExactly("bluetooth_device_header", "action_buttons", "keyboard_settings")
+ }
+ }
+
+ @Test
+ fun updateLayout_itemsNotInConfig_hide() {
+ testScope.runTest {
+ `when`(repository.getDeviceSettingsConfig(cachedDevice))
+ .thenReturn(
+ DeviceSettingConfigModel(
+ listOf(
+ DeviceSettingConfigItemModel.BuiltinItem(
+ DeviceSettingId.DEVICE_SETTING_ID_HEADER,
+ "bluetooth_device_header"),
+ DeviceSettingConfigItemModel.BuiltinItem(
+ DeviceSettingId.DEVICE_SETTING_ID_KEYBOARD_SETTINGS,
+ "keyboard_settings"),
+ ),
+ listOf(),
+ "footer"))
+
+ underTest.updateLayout()
+
+ assertThat(getDisplayedPreferences().map { it.key })
+ .containsExactly("bluetooth_device_header", "keyboard_settings")
+ }
+ }
+
+ @Test
+ fun updateLayout_newItems_displayNewItems() {
+ testScope.runTest {
+ `when`(repository.getDeviceSettingsConfig(cachedDevice))
+ .thenReturn(
+ DeviceSettingConfigModel(
+ listOf(
+ DeviceSettingConfigItemModel.BuiltinItem(
+ DeviceSettingId.DEVICE_SETTING_ID_HEADER,
+ "bluetooth_device_header"),
+ DeviceSettingConfigItemModel.AppProvidedItem(
+ DeviceSettingId.DEVICE_SETTING_ID_ANC),
+ DeviceSettingConfigItemModel.BuiltinItem(
+ DeviceSettingId.DEVICE_SETTING_ID_KEYBOARD_SETTINGS,
+ "keyboard_settings"),
+ ),
+ listOf(),
+ "footer"))
+ `when`(repository.getDeviceSetting(cachedDevice, DeviceSettingId.DEVICE_SETTING_ID_ANC))
+ .thenReturn(
+ flowOf(
+ DeviceSettingModel.MultiTogglePreference(
+ cachedDevice,
+ DeviceSettingId.DEVICE_SETTING_ID_ANC,
+ "title",
+ toggles =
+ listOf(
+ ToggleModel(
+ "", Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888))),
+ isActive = true,
+ state = DeviceSettingStateModel.MultiTogglePreferenceState(0),
+ isAllowedChangingState = true,
+ updateState = {})))
+
+ underTest.updateLayout()
+
+ assertThat(getDisplayedPreferences().map { it.key })
+ .containsExactly(
+ "bluetooth_device_header",
+ "DEVICE_SETTING_${DeviceSettingId.DEVICE_SETTING_ID_ANC}",
+ "keyboard_settings")
+ }
+ }
+
+ private fun getDisplayedPreferences(): List<Preference> {
+ val prefs = mutableListOf<Preference>()
+ for (i in 0..<fragment.preferenceScreen.preferenceCount) {
+ prefs.add(fragment.preferenceScreen.getPreference(i))
+ }
+ return prefs
+ }
+
+ class TestFragment(context: Context) : DashboardFragment() {
+ private val mPreferenceManager: PreferenceManager = PreferenceManager(context)
+
+ init {
+ mPreferenceManager.setPreferences(mPreferenceManager.createPreferenceScreen(context))
+ }
+
+ public override fun getPreferenceScreenResId(): Int = 0
+
+ override fun getLogTag(): String = "TestLogTag"
+
+ override fun getPreferenceScreen(): PreferenceScreen {
+ return mPreferenceManager.preferenceScreen
+ }
+
+ override fun getMetricsCategory(): Int = 0
+
+ override fun getPreferenceManager(): PreferenceManager {
+ return mPreferenceManager
+ }
+ }
+
+ private companion object {}
+}
diff --git a/tests/robotests/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModelTest.kt b/tests/robotests/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModelTest.kt
new file mode 100644
index 0000000..cc462bb
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/bluetooth/ui/viewmodel/BluetoothDeviceDetailsViewModelTest.kt
@@ -0,0 +1,186 @@
+/*
+ * 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.viewmodel
+
+import android.bluetooth.BluetoothAdapter
+import android.content.Context
+import android.graphics.Bitmap
+import androidx.test.core.app.ApplicationProvider
+import com.android.settings.bluetooth.ui.layout.DeviceSettingLayout
+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.DeviceSettingConfigItemModel
+import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingConfigModel
+import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel
+import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel
+import com.android.settingslib.bluetooth.devicesettings.shared.model.ToggleModel
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.robolectric.RobolectricTestRunner
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(RobolectricTestRunner::class)
+class BluetoothDeviceDetailsViewModelTest {
+ @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+ @Mock private lateinit var cachedDevice: CachedBluetoothDevice
+
+ @Mock private lateinit var bluetoothAdapter: BluetoothAdapter
+
+ @Mock private lateinit var repository: DeviceSettingRepository
+
+ private lateinit var underTest: BluetoothDeviceDetailsViewModel
+ private lateinit var featureFactory: FakeFeatureFactory
+ private val testScope = TestScope()
+
+ @Before
+ fun setUp() {
+ val context = ApplicationProvider.getApplicationContext<Context>()
+ featureFactory = FakeFeatureFactory.setupForTest()
+ `when`(
+ featureFactory.bluetoothFeatureProvider.getDeviceSettingRepository(
+ eq(context), eq(bluetoothAdapter), any()))
+ .thenReturn(repository)
+
+ underTest = BluetoothDeviceDetailsViewModel(repository, cachedDevice)
+ }
+
+ @Test
+ fun getItems_returnConfigMainItems() {
+ testScope.runTest {
+ `when`(repository.getDeviceSettingsConfig(cachedDevice))
+ .thenReturn(
+ DeviceSettingConfigModel(
+ listOf(BUILTIN_SETTING_ITEM_1, BUILDIN_SETTING_ITEM_2), listOf(), "footer"))
+
+ val keys = underTest.getItems()
+
+ assertThat(keys).containsExactly(BUILTIN_SETTING_ITEM_1, BUILDIN_SETTING_ITEM_2)
+ }
+ }
+
+ @Test
+ fun getLayout_builtinDeviceSettings() {
+ testScope.runTest {
+ `when`(repository.getDeviceSettingsConfig(cachedDevice))
+ .thenReturn(
+ DeviceSettingConfigModel(
+ listOf(BUILTIN_SETTING_ITEM_1, BUILDIN_SETTING_ITEM_2), listOf(), "footer"))
+
+ val layout = underTest.getLayout()!!
+
+ 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(),
+ "footer"))
+ `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()!!
+
+ assertThat(getLatestLayout(layout))
+ .isEqualTo(
+ listOf(
+ listOf(DeviceSettingId.DEVICE_SETTING_ID_HEADER),
+ listOf(remoteSettingId1, remoteSettingId2),
+ listOf(remoteSettingId3),
+ ))
+ }
+ }
+
+ private fun getLatestLayout(layout: DeviceSettingLayout): List<List<Int>> {
+ var latestLayout = MutableList(layout.rows.size) { emptyList<Int>() }
+ for (i in layout.rows.indices) {
+ layout.rows[i]
+ .settingIds
+ .onEach { latestLayout[i] = it }
+ .launchIn(testScope.backgroundScope)
+ }
+
+ testScope.runCurrent()
+ return latestLayout.filter { !it.isEmpty() }.toList()
+ }
+
+ private fun buildMultiTogglePreference(settingId: Int) =
+ DeviceSettingModel.MultiTogglePreference(
+ cachedDevice,
+ settingId,
+ "title",
+ toggles = listOf(ToggleModel("", Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888))),
+ isActive = true,
+ state = DeviceSettingStateModel.MultiTogglePreferenceState(0),
+ isAllowedChangingState = true,
+ updateState = {})
+
+ private fun buildActionSwitchPreference(settingId: Int) =
+ DeviceSettingModel.ActionSwitchPreference(cachedDevice, settingId, "title")
+
+ private fun buildRemoteSettingItem(settingId: Int) =
+ DeviceSettingConfigItemModel.AppProvidedItem(settingId)
+
+ private companion object {
+ val BUILTIN_SETTING_ITEM_1 =
+ DeviceSettingConfigItemModel.BuiltinItem(
+ DeviceSettingId.DEVICE_SETTING_ID_HEADER, "bluetooth_device_header")
+ val BUILDIN_SETTING_ITEM_2 =
+ DeviceSettingConfigItemModel.BuiltinItem(
+ DeviceSettingId.DEVICE_SETTING_ID_ACTION_BUTTONS, "action_buttons")
+ }
+}