Merge "Revert ANC and Spatial audio UI change" into main
This commit is contained in:
@@ -20,7 +20,6 @@ import android.bluetooth.BluetoothAdapter;
|
|||||||
import android.bluetooth.BluetoothDevice;
|
import android.bluetooth.BluetoothDevice;
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.media.AudioManager;
|
|
||||||
import android.media.Spatializer;
|
import android.media.Spatializer;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
|
||||||
@@ -28,7 +27,6 @@ import androidx.annotation.NonNull;
|
|||||||
import androidx.preference.Preference;
|
import androidx.preference.Preference;
|
||||||
|
|
||||||
import com.android.settings.SettingsPreferenceFragment;
|
import com.android.settings.SettingsPreferenceFragment;
|
||||||
import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractor;
|
|
||||||
import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatter;
|
import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatter;
|
||||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
|
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
|
||||||
import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository;
|
import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository;
|
||||||
@@ -98,13 +96,6 @@ public interface BluetoothFeatureProvider {
|
|||||||
@NonNull BluetoothAdapter bluetoothAdapter,
|
@NonNull BluetoothAdapter bluetoothAdapter,
|
||||||
@NonNull CoroutineScope scope);
|
@NonNull CoroutineScope scope);
|
||||||
|
|
||||||
/** Gets spatial audio interactor. */
|
|
||||||
@NonNull
|
|
||||||
SpatialAudioInteractor getSpatialAudioInteractor(
|
|
||||||
@NonNull Context context,
|
|
||||||
@NonNull AudioManager audioManager,
|
|
||||||
@NonNull CoroutineScope scope);
|
|
||||||
|
|
||||||
/** Gets device details fragment layout formatter. */
|
/** Gets device details fragment layout formatter. */
|
||||||
@NonNull
|
@NonNull
|
||||||
DeviceDetailsFragmentFormatter getDeviceDetailsFragmentFormatter(
|
DeviceDetailsFragmentFormatter getDeviceDetailsFragmentFormatter(
|
||||||
|
@@ -22,20 +22,14 @@ import android.content.Context
|
|||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.media.Spatializer
|
import android.media.Spatializer
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
|
||||||
import androidx.lifecycle.LifecycleCoroutineScope
|
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import com.android.settings.SettingsPreferenceFragment
|
import com.android.settings.SettingsPreferenceFragment
|
||||||
import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractor
|
|
||||||
import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractorImpl
|
|
||||||
import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatter
|
import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatter
|
||||||
import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatterImpl
|
import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatterImpl
|
||||||
import com.android.settingslib.bluetooth.BluetoothUtils
|
import com.android.settingslib.bluetooth.BluetoothUtils
|
||||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice
|
import com.android.settingslib.bluetooth.CachedBluetoothDevice
|
||||||
import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository
|
import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository
|
||||||
import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepositoryImpl
|
import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepositoryImpl
|
||||||
import com.android.settingslib.media.data.repository.SpatializerRepositoryImpl
|
|
||||||
import com.android.settingslib.media.domain.interactor.SpatializerInteractor
|
|
||||||
import com.google.common.collect.ImmutableList
|
import com.google.common.collect.ImmutableList
|
||||||
import com.google.common.collect.ImmutableSet
|
import com.google.common.collect.ImmutableSet
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -82,21 +76,6 @@ open class BluetoothFeatureProviderImpl : BluetoothFeatureProvider {
|
|||||||
): DeviceSettingRepository =
|
): DeviceSettingRepository =
|
||||||
DeviceSettingRepositoryImpl(context, bluetoothAdapter, scope, Dispatchers.IO)
|
DeviceSettingRepositoryImpl(context, bluetoothAdapter, scope, Dispatchers.IO)
|
||||||
|
|
||||||
override fun getSpatialAudioInteractor(
|
|
||||||
context: Context,
|
|
||||||
audioManager: AudioManager,
|
|
||||||
scope: CoroutineScope,
|
|
||||||
): SpatialAudioInteractor {
|
|
||||||
return SpatialAudioInteractorImpl(
|
|
||||||
context, audioManager,
|
|
||||||
SpatializerInteractor(
|
|
||||||
SpatializerRepositoryImpl(
|
|
||||||
getSpatializer(context),
|
|
||||||
Dispatchers.IO
|
|
||||||
)
|
|
||||||
), scope, Dispatchers.IO)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getDeviceDetailsFragmentFormatter(
|
override fun getDeviceDetailsFragmentFormatter(
|
||||||
context: Context,
|
context: Context,
|
||||||
fragment: SettingsPreferenceFragment,
|
fragment: SettingsPreferenceFragment,
|
||||||
|
@@ -1,180 +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.domain.interactor
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.media.AudioManager
|
|
||||||
import android.util.Log
|
|
||||||
import com.android.settings.R
|
|
||||||
import com.android.settingslib.bluetooth.BluetoothUtils
|
|
||||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice
|
|
||||||
import com.android.settingslib.bluetooth.devicesettings.DeviceSettingId
|
|
||||||
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingIcon
|
|
||||||
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.android.settingslib.media.domain.interactor.SpatializerInteractor
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
|
||||||
import kotlinx.coroutines.flow.callbackFlow
|
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import kotlinx.coroutines.flow.flowOn
|
|
||||||
import kotlinx.coroutines.flow.onStart
|
|
||||||
import kotlinx.coroutines.flow.stateIn
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
/** Provides device setting for spatial audio. */
|
|
||||||
interface SpatialAudioInteractor {
|
|
||||||
/** Gets device setting for spatial audio */
|
|
||||||
fun getDeviceSetting(cachedDevice: CachedBluetoothDevice): Flow<DeviceSettingModel?>
|
|
||||||
}
|
|
||||||
|
|
||||||
class SpatialAudioInteractorImpl(
|
|
||||||
private val context: Context,
|
|
||||||
private val audioManager: AudioManager,
|
|
||||||
private val spatializerInteractor: SpatializerInteractor,
|
|
||||||
private val coroutineScope: CoroutineScope,
|
|
||||||
private val backgroundCoroutineContext: CoroutineContext,
|
|
||||||
) : SpatialAudioInteractor {
|
|
||||||
private val spatialAudioOffToggle =
|
|
||||||
ToggleModel(
|
|
||||||
context.getString(R.string.spatial_audio_multi_toggle_off),
|
|
||||||
DeviceSettingIcon.ResourceIcon(R.drawable.ic_spatial_audio_off),
|
|
||||||
)
|
|
||||||
private val spatialAudioOnToggle =
|
|
||||||
ToggleModel(
|
|
||||||
context.getString(R.string.spatial_audio_multi_toggle_on),
|
|
||||||
DeviceSettingIcon.ResourceIcon(R.drawable.ic_spatial_audio),
|
|
||||||
)
|
|
||||||
private val headTrackingOnToggle =
|
|
||||||
ToggleModel(
|
|
||||||
context.getString(R.string.spatial_audio_multi_toggle_head_tracking_on),
|
|
||||||
DeviceSettingIcon.ResourceIcon(R.drawable.ic_head_tracking),
|
|
||||||
)
|
|
||||||
private val changes = MutableSharedFlow<Unit>()
|
|
||||||
|
|
||||||
override fun getDeviceSetting(cachedDevice: CachedBluetoothDevice): Flow<DeviceSettingModel?> =
|
|
||||||
changes
|
|
||||||
.onStart { emit(Unit) }
|
|
||||||
.combine(
|
|
||||||
isDeviceConnected(cachedDevice),
|
|
||||||
) { _, connected ->
|
|
||||||
if (connected) {
|
|
||||||
getSpatialAudioDeviceSettingModel(cachedDevice)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.flowOn(backgroundCoroutineContext)
|
|
||||||
.stateIn(coroutineScope, SharingStarted.WhileSubscribed(), initialValue = null)
|
|
||||||
|
|
||||||
private fun isDeviceConnected(cachedDevice: CachedBluetoothDevice): Flow<Boolean> =
|
|
||||||
callbackFlow {
|
|
||||||
val listener =
|
|
||||||
CachedBluetoothDevice.Callback { launch { send(cachedDevice.isConnected) } }
|
|
||||||
cachedDevice.registerCallback(context.mainExecutor, listener)
|
|
||||||
awaitClose { cachedDevice.unregisterCallback(listener) }
|
|
||||||
}
|
|
||||||
.onStart { emit(cachedDevice.isConnected) }
|
|
||||||
.flowOn(backgroundCoroutineContext)
|
|
||||||
|
|
||||||
private suspend fun getSpatialAudioDeviceSettingModel(
|
|
||||||
cachedDevice: CachedBluetoothDevice
|
|
||||||
): DeviceSettingModel? {
|
|
||||||
// TODO(b/343317785): use audio repository instead of calling AudioManager directly.
|
|
||||||
Log.i(TAG, "CachedDevice: $cachedDevice profiles: ${cachedDevice.profiles}")
|
|
||||||
val attributes =
|
|
||||||
BluetoothUtils.getAudioDeviceAttributesForSpatialAudio(
|
|
||||||
cachedDevice,
|
|
||||||
audioManager.getBluetoothAudioDeviceCategory(cachedDevice.address),
|
|
||||||
)
|
|
||||||
?: run {
|
|
||||||
Log.i(TAG, "No audio profiles in cachedDevice: ${cachedDevice.address}.")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.i(TAG, "Audio device attributes for ${cachedDevice.address}: $attributes.")
|
|
||||||
val spatialAudioAvailable = spatializerInteractor.isSpatialAudioAvailable(attributes)
|
|
||||||
if (!spatialAudioAvailable) {
|
|
||||||
Log.i(TAG, "Spatial audio is not available for ${cachedDevice.address}")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
val headTrackingAvailable =
|
|
||||||
spatialAudioAvailable && spatializerInteractor.isHeadTrackingAvailable(attributes)
|
|
||||||
val toggles =
|
|
||||||
if (headTrackingAvailable) {
|
|
||||||
listOf(spatialAudioOffToggle, spatialAudioOnToggle, headTrackingOnToggle)
|
|
||||||
} else {
|
|
||||||
listOf(spatialAudioOffToggle, spatialAudioOnToggle)
|
|
||||||
}
|
|
||||||
val spatialAudioEnabled = spatializerInteractor.isSpatialAudioEnabled(attributes)
|
|
||||||
val headTrackingEnabled =
|
|
||||||
spatialAudioEnabled && spatializerInteractor.isHeadTrackingEnabled(attributes)
|
|
||||||
|
|
||||||
val activeIndex =
|
|
||||||
when {
|
|
||||||
headTrackingEnabled -> INDEX_HEAD_TRACKING_ENABLED
|
|
||||||
spatialAudioEnabled -> INDEX_SPATIAL_AUDIO_ON
|
|
||||||
else -> INDEX_SPATIAL_AUDIO_OFF
|
|
||||||
}
|
|
||||||
Log.i(
|
|
||||||
TAG,
|
|
||||||
"Head tracking available: $headTrackingAvailable, " +
|
|
||||||
"spatial audio enabled: $spatialAudioEnabled, " +
|
|
||||||
"head tracking enabled: $headTrackingEnabled",
|
|
||||||
)
|
|
||||||
return DeviceSettingModel.MultiTogglePreference(
|
|
||||||
cachedDevice = cachedDevice,
|
|
||||||
id = DeviceSettingId.DEVICE_SETTING_ID_SPATIAL_AUDIO_MULTI_TOGGLE,
|
|
||||||
title = context.getString(R.string.spatial_audio_multi_toggle_title),
|
|
||||||
toggles = toggles,
|
|
||||||
isActive = spatialAudioEnabled,
|
|
||||||
state = DeviceSettingStateModel.MultiTogglePreferenceState(activeIndex),
|
|
||||||
isAllowedChangingState = true,
|
|
||||||
updateState = { newState ->
|
|
||||||
coroutineScope.launch(backgroundCoroutineContext) {
|
|
||||||
Log.i(TAG, "Update spatial audio state: $newState")
|
|
||||||
when (newState.selectedIndex) {
|
|
||||||
INDEX_SPATIAL_AUDIO_OFF -> {
|
|
||||||
spatializerInteractor.setSpatialAudioEnabled(attributes, false)
|
|
||||||
}
|
|
||||||
INDEX_SPATIAL_AUDIO_ON -> {
|
|
||||||
spatializerInteractor.setSpatialAudioEnabled(attributes, true)
|
|
||||||
spatializerInteractor.setHeadTrackingEnabled(attributes, false)
|
|
||||||
}
|
|
||||||
INDEX_HEAD_TRACKING_ENABLED -> {
|
|
||||||
spatializerInteractor.setSpatialAudioEnabled(attributes, true)
|
|
||||||
spatializerInteractor.setHeadTrackingEnabled(attributes, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
changes.emit(Unit)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val TAG = "SpatialAudioInteractor"
|
|
||||||
private const val INDEX_SPATIAL_AUDIO_OFF = 0
|
|
||||||
private const val INDEX_SPATIAL_AUDIO_ON = 1
|
|
||||||
private const val INDEX_HEAD_TRACKING_ENABLED = 2
|
|
||||||
}
|
|
||||||
}
|
|
@@ -0,0 +1,124 @@
|
|||||||
|
/*
|
||||||
|
* 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.composable
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.defaultMinSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
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.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.android.settings.bluetooth.ui.composable.Icon as DeviceSettingComposeIcon
|
||||||
|
import com.android.settings.bluetooth.ui.model.DeviceSettingPreferenceModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MultiTogglePreference(pref: DeviceSettingPreferenceModel.MultiTogglePreference) {
|
||||||
|
Column(modifier = Modifier.padding(24.dp)) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().height(56.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
|
) {
|
||||||
|
Box {
|
||||||
|
Row {
|
||||||
|
for ((idx, toggle) in pref.toggles.withIndex()) {
|
||||||
|
val selected = idx == pref.selectedIndex
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
.padding(start = if (idx == 0) 0.dp else 1.dp)
|
||||||
|
.height(56.dp)
|
||||||
|
.background(
|
||||||
|
Color.Transparent,
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
val startCornerRadius = if (idx == 0) 12.dp else 0.dp
|
||||||
|
val endCornerRadius = if (idx == pref.toggles.size - 1) 12.dp else 0.dp
|
||||||
|
Button(
|
||||||
|
onClick = { pref.onSelectedChange(idx) },
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
enabled = pref.isAllowedChangingState,
|
||||||
|
colors = getButtonColors(selected),
|
||||||
|
shape = RoundedCornerShape(
|
||||||
|
startCornerRadius,
|
||||||
|
endCornerRadius,
|
||||||
|
endCornerRadius,
|
||||||
|
startCornerRadius,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
DeviceSettingComposeIcon(
|
||||||
|
toggle.icon,
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().defaultMinSize(32.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
|
) {
|
||||||
|
for (toggle in pref.toggles) {
|
||||||
|
Text(
|
||||||
|
text = toggle.label,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
overflow = TextOverflow.Visible,
|
||||||
|
modifier = Modifier.weight(1f).padding(horizontal = 8.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun getButtonColors(isActive: Boolean) = if (isActive) {
|
||||||
|
ButtonDefaults.buttonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.primary,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onPrimary,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ButtonDefaults.buttonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
)
|
||||||
|
}
|
@@ -1,280 +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.composable
|
|
||||||
|
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.defaultMinSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.offset
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.BasicAlertDialog
|
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.ButtonDefaults
|
|
||||||
import androidx.compose.material3.Card
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.LocalContentColor
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.geometry.Rect
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.layout.boundsInParent
|
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.semantics.Role
|
|
||||||
import androidx.compose.ui.semantics.contentDescription
|
|
||||||
import androidx.compose.ui.semantics.role
|
|
||||||
import androidx.compose.ui.semantics.semantics
|
|
||||||
import androidx.compose.ui.semantics.toggleableState
|
|
||||||
import androidx.compose.ui.state.ToggleableState
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import androidx.compose.ui.window.DialogProperties
|
|
||||||
import com.android.settings.R
|
|
||||||
import com.android.settings.bluetooth.ui.model.DeviceSettingPreferenceModel
|
|
||||||
import com.android.settings.bluetooth.ui.composable.Icon as DeviceSettingComposeIcon
|
|
||||||
import com.android.settingslib.spa.framework.theme.SettingsDimension
|
|
||||||
import com.android.settingslib.spa.widget.dialog.getDialogWidth
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MultiTogglePreferenceGroup(
|
|
||||||
preferenceModels: List<DeviceSettingPreferenceModel.MultiTogglePreference>,
|
|
||||||
) {
|
|
||||||
var settingIdForPopUp by remember { mutableStateOf<Int?>(null) }
|
|
||||||
|
|
||||||
settingIdForPopUp?.let { id ->
|
|
||||||
preferenceModels.find { it.id == id && it.isAllowedChangingState }?.let {
|
|
||||||
dialog(it) { settingIdForPopUp = null }
|
|
||||||
} ?: run {
|
|
||||||
settingIdForPopUp = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.padding(SettingsDimension.itemPadding),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(24.dp),
|
|
||||||
) {
|
|
||||||
preferenceModels.forEach { preferenceModel ->
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
verticalArrangement = Arrangement.Top,
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
) {
|
|
||||||
Row {
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier.height(64.dp),
|
|
||||||
shape = RoundedCornerShape(28.dp),
|
|
||||||
color = MaterialTheme.colorScheme.surface) {
|
|
||||||
Button(
|
|
||||||
modifier =
|
|
||||||
Modifier.fillMaxSize().padding(8.dp).semantics {
|
|
||||||
role = Role.Switch
|
|
||||||
toggleableState =
|
|
||||||
if (!preferenceModel.isAllowedChangingState) {
|
|
||||||
ToggleableState.Indeterminate
|
|
||||||
} else if (preferenceModel.isActive) {
|
|
||||||
ToggleableState.On
|
|
||||||
} else {
|
|
||||||
ToggleableState.Off
|
|
||||||
}
|
|
||||||
contentDescription = preferenceModel.title
|
|
||||||
},
|
|
||||||
onClick = { settingIdForPopUp = preferenceModel.id },
|
|
||||||
enabled = preferenceModel.isAllowedChangingState,
|
|
||||||
shape = RoundedCornerShape(20.dp),
|
|
||||||
colors = getButtonColors(preferenceModel.isActive),
|
|
||||||
contentPadding = PaddingValues(0.dp)) {
|
|
||||||
DeviceSettingComposeIcon(
|
|
||||||
preferenceModel.toggles[preferenceModel.selectedIndex]
|
|
||||||
.icon,
|
|
||||||
modifier = Modifier.size(24.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Row { Text(text = preferenceModel.title, fontSize = 12.sp) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun getButtonColors(isActive: Boolean) =
|
|
||||||
if (isActive) {
|
|
||||||
ButtonDefaults.buttonColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.tertiaryContainer,
|
|
||||||
contentColor = MaterialTheme.colorScheme.onTertiaryContainer,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
ButtonDefaults.buttonColors(
|
|
||||||
containerColor = Color.Transparent,
|
|
||||||
contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
private fun dialog(
|
|
||||||
multiTogglePreference: DeviceSettingPreferenceModel.MultiTogglePreference,
|
|
||||||
onDismiss: () -> Unit
|
|
||||||
) {
|
|
||||||
BasicAlertDialog(
|
|
||||||
onDismissRequest = { onDismiss() },
|
|
||||||
modifier = Modifier.width(getDialogWidth()),
|
|
||||||
properties = DialogProperties(usePlatformDefaultWidth = false),
|
|
||||||
content = {
|
|
||||||
Card(
|
|
||||||
shape = RoundedCornerShape(28.dp),
|
|
||||||
modifier = Modifier.fillMaxWidth().height(192.dp),
|
|
||||||
content = {
|
|
||||||
Box {
|
|
||||||
Button(
|
|
||||||
onClick = { onDismiss() },
|
|
||||||
modifier = Modifier.padding(8.dp).align(Alignment.TopEnd).size(48.dp),
|
|
||||||
contentPadding = PaddingValues(12.dp),
|
|
||||||
colors =
|
|
||||||
ButtonDefaults.buttonColors(containerColor = Color.Transparent),
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
painterResource(id = R.drawable.ic_close),
|
|
||||||
null,
|
|
||||||
tint = MaterialTheme.colorScheme.inverseSurface)
|
|
||||||
}
|
|
||||||
Box(modifier = Modifier.padding(horizontal = 8.dp, vertical = 20.dp)) {
|
|
||||||
dialogContent(multiTogglePreference)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun dialogContent(multiTogglePreference: DeviceSettingPreferenceModel.MultiTogglePreference) {
|
|
||||||
Column {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth().height(24.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
|
||||||
) {
|
|
||||||
Text(text = multiTogglePreference.title, fontSize = 16.sp)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(20.dp))
|
|
||||||
var selectedRect by remember { mutableStateOf<Rect?>(null) }
|
|
||||||
val offset =
|
|
||||||
selectedRect?.let { rect ->
|
|
||||||
animateFloatAsState(targetValue = rect.left, finishedListener = {}).value
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier =
|
|
||||||
Modifier.fillMaxWidth()
|
|
||||||
.height(64.dp)
|
|
||||||
.background(
|
|
||||||
MaterialTheme.colorScheme.surface, shape = RoundedCornerShape(28.dp)),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
|
||||||
) {
|
|
||||||
Box {
|
|
||||||
offset?.let { offset ->
|
|
||||||
with(LocalDensity.current) {
|
|
||||||
Box(
|
|
||||||
modifier =
|
|
||||||
Modifier.offset(offset.toDp(), 0.dp)
|
|
||||||
.height(selectedRect!!.height.toDp())
|
|
||||||
.width(selectedRect!!.width.toDp())
|
|
||||||
.background(
|
|
||||||
MaterialTheme.colorScheme.tertiaryContainer,
|
|
||||||
shape = RoundedCornerShape(20.dp)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Row {
|
|
||||||
for ((idx, toggle) in multiTogglePreference.toggles.withIndex()) {
|
|
||||||
val selected = idx == multiTogglePreference.selectedIndex
|
|
||||||
Column(
|
|
||||||
modifier =
|
|
||||||
Modifier.weight(1f)
|
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
.height(48.dp)
|
|
||||||
.background(
|
|
||||||
Color.Transparent, shape = RoundedCornerShape(28.dp))
|
|
||||||
.onGloballyPositioned { layoutCoordinates ->
|
|
||||||
if (selected) {
|
|
||||||
selectedRect = layoutCoordinates.boundsInParent()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
) {
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
multiTogglePreference.onSelectedChange(idx)
|
|
||||||
},
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
colors =
|
|
||||||
ButtonDefaults.buttonColors(
|
|
||||||
containerColor = Color.Transparent,
|
|
||||||
contentColor = LocalContentColor.current),
|
|
||||||
) {
|
|
||||||
DeviceSettingComposeIcon(
|
|
||||||
toggle.icon, modifier = Modifier.size(24.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth().defaultMinSize(32.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
|
||||||
) {
|
|
||||||
for (toggle in multiTogglePreference.toggles) {
|
|
||||||
Text(
|
|
||||||
text = toggle.label,
|
|
||||||
fontSize = 12.sp,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
overflow = TextOverflow.Visible,
|
|
||||||
modifier = Modifier.weight(1f).padding(horizontal = 8.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -43,7 +43,7 @@ import androidx.preference.Preference
|
|||||||
import com.android.settings.R
|
import com.android.settings.R
|
||||||
import com.android.settings.SettingsPreferenceFragment
|
import com.android.settings.SettingsPreferenceFragment
|
||||||
import com.android.settings.bluetooth.ui.composable.Icon
|
import com.android.settings.bluetooth.ui.composable.Icon
|
||||||
import com.android.settings.bluetooth.ui.composable.MultiTogglePreferenceGroup
|
import com.android.settings.bluetooth.ui.composable.MultiTogglePreference
|
||||||
import com.android.settings.bluetooth.ui.layout.DeviceSettingLayout
|
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
|
||||||
@@ -56,11 +56,14 @@ import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSetti
|
|||||||
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.DeviceSettingIcon
|
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingIcon
|
||||||
import com.android.settingslib.spa.framework.theme.SettingsDimension
|
import com.android.settingslib.spa.framework.theme.SettingsDimension
|
||||||
|
import com.android.settingslib.spa.widget.button.ActionButton
|
||||||
|
import com.android.settingslib.spa.widget.button.ActionButtons
|
||||||
import com.android.settingslib.spa.widget.preference.Preference as SpaPreference
|
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.PreferenceModel
|
||||||
import com.android.settingslib.spa.widget.preference.SwitchPreference
|
import com.android.settingslib.spa.widget.preference.SwitchPreference
|
||||||
import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
|
import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
|
||||||
import com.android.settingslib.spa.widget.preference.TwoTargetSwitchPreference
|
import com.android.settingslib.spa.widget.preference.TwoTargetSwitchPreference
|
||||||
|
import com.android.settingslib.spa.widget.scaffold.RegularScaffold
|
||||||
import com.android.settingslib.spa.widget.ui.Footer
|
import com.android.settingslib.spa.widget.ui.Footer
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
@@ -241,7 +244,7 @@ class DeviceDetailsFragmentFormatterImpl(
|
|||||||
buildSwitchPreference(setting)
|
buildSwitchPreference(setting)
|
||||||
}
|
}
|
||||||
is DeviceSettingPreferenceModel.MultiTogglePreference -> {
|
is DeviceSettingPreferenceModel.MultiTogglePreference -> {
|
||||||
buildMultiTogglePreference(listOf(setting))
|
buildMultiTogglePreference(setting)
|
||||||
}
|
}
|
||||||
is DeviceSettingPreferenceModel.FooterPreference -> {
|
is DeviceSettingPreferenceModel.FooterPreference -> {
|
||||||
buildFooterPreference(setting)
|
buildFooterPreference(setting)
|
||||||
@@ -253,22 +256,15 @@ class DeviceDetailsFragmentFormatterImpl(
|
|||||||
null -> {}
|
null -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {}
|
||||||
if (!settings.all { it is DeviceSettingPreferenceModel.MultiTogglePreference }) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
buildMultiTogglePreference(
|
|
||||||
settings.filterIsInstance<DeviceSettingPreferenceModel.MultiTogglePreference>()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun buildMultiTogglePreference(
|
private fun buildMultiTogglePreference(
|
||||||
prefs: List<DeviceSettingPreferenceModel.MultiTogglePreference>
|
pref: DeviceSettingPreferenceModel.MultiTogglePreference
|
||||||
) {
|
) {
|
||||||
MultiTogglePreferenceGroup(prefs)
|
MultiTogglePreference(pref)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@@ -18,8 +18,6 @@ package com.android.settings.bluetooth.ui.viewmodel
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.bluetooth.BluetoothAdapter
|
import android.bluetooth.BluetoothAdapter
|
||||||
import android.media.AudioManager
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
@@ -60,20 +58,12 @@ class BluetoothDeviceDetailsViewModel(
|
|||||||
bluetoothAdapter,
|
bluetoothAdapter,
|
||||||
viewModelScope,
|
viewModelScope,
|
||||||
)
|
)
|
||||||
private val spatialAudioInteractor =
|
|
||||||
featureFactory.bluetoothFeatureProvider.getSpatialAudioInteractor(
|
|
||||||
application,
|
|
||||||
application.getSystemService(AudioManager::class.java),
|
|
||||||
viewModelScope,
|
|
||||||
)
|
|
||||||
|
|
||||||
private val items =
|
private val items =
|
||||||
viewModelScope.async(backgroundCoroutineContext, start = CoroutineStart.LAZY) {
|
viewModelScope.async(backgroundCoroutineContext, start = CoroutineStart.LAZY) {
|
||||||
deviceSettingRepository.getDeviceSettingsConfig(cachedDevice)
|
deviceSettingRepository.getDeviceSettingsConfig(cachedDevice)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val spatialAudioModel by lazy { spatialAudioInteractor.getDeviceSetting(cachedDevice) }
|
|
||||||
|
|
||||||
suspend fun getItems(fragment: FragmentTypeModel): List<DeviceSettingConfigItemModel>? =
|
suspend fun getItems(fragment: FragmentTypeModel): List<DeviceSettingConfigItemModel>? =
|
||||||
when (fragment) {
|
when (fragment) {
|
||||||
is FragmentTypeModel.DeviceDetailsMainFragment -> items.await()?.mainItems
|
is FragmentTypeModel.DeviceDetailsMainFragment -> items.await()?.mainItems
|
||||||
@@ -95,11 +85,8 @@ class BluetoothDeviceDetailsViewModel(
|
|||||||
if (settingId == DeviceSettingId.DEVICE_SETTING_ID_MORE_SETTINGS) {
|
if (settingId == DeviceSettingId.DEVICE_SETTING_ID_MORE_SETTINGS) {
|
||||||
return flowOf(DeviceSettingPreferenceModel.MoreSettingsPreference(settingId))
|
return flowOf(DeviceSettingPreferenceModel.MoreSettingsPreference(settingId))
|
||||||
}
|
}
|
||||||
return when (settingId) {
|
return deviceSettingRepository.getDeviceSetting(cachedDevice, settingId)
|
||||||
DeviceSettingId.DEVICE_SETTING_ID_SPATIAL_AUDIO_MULTI_TOGGLE ->
|
.map { it?.toPreferenceModel() }
|
||||||
spatialAudioModel
|
|
||||||
else -> deviceSettingRepository.getDeviceSetting(cachedDevice, settingId)
|
|
||||||
}.map { it?.toPreferenceModel() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun DeviceSettingModel.toPreferenceModel(): DeviceSettingPreferenceModel? {
|
private fun DeviceSettingModel.toPreferenceModel(): DeviceSettingPreferenceModel? {
|
||||||
@@ -166,7 +153,6 @@ class BluetoothDeviceDetailsViewModel(
|
|||||||
val positionToSettingIds =
|
val positionToSettingIds =
|
||||||
combine(configDeviceSetting) { settings ->
|
combine(configDeviceSetting) { settings ->
|
||||||
val positionMapping = mutableMapOf<Int, List<DeviceSettingLayoutColumn>>()
|
val positionMapping = mutableMapOf<Int, List<DeviceSettingLayoutColumn>>()
|
||||||
var multiToggleSettingIds: MutableList<DeviceSettingLayoutColumn>? = null
|
|
||||||
for (i in settings.indices) {
|
for (i in settings.indices) {
|
||||||
val configItem = configItems[i]
|
val configItem = configItems[i]
|
||||||
val setting = settings[i]
|
val setting = settings[i]
|
||||||
@@ -174,35 +160,13 @@ class BluetoothDeviceDetailsViewModel(
|
|||||||
if (!isXmlPreference && setting == null) {
|
if (!isXmlPreference && setting == null) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (setting !is DeviceSettingPreferenceModel.MultiTogglePreference) {
|
positionMapping[i] =
|
||||||
multiToggleSettingIds = null
|
listOf(
|
||||||
positionMapping[i] =
|
|
||||||
listOf(
|
|
||||||
DeviceSettingLayoutColumn(
|
|
||||||
configItem.settingId,
|
|
||||||
configItem.highlighted,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (multiToggleSettingIds != null) {
|
|
||||||
multiToggleSettingIds.add(
|
|
||||||
DeviceSettingLayoutColumn(
|
DeviceSettingLayoutColumn(
|
||||||
configItem.settingId,
|
configItem.settingId,
|
||||||
configItem.highlighted,
|
configItem.highlighted,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
multiToggleSettingIds =
|
|
||||||
mutableListOf(
|
|
||||||
DeviceSettingLayoutColumn(
|
|
||||||
configItem.settingId,
|
|
||||||
configItem.highlighted,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
positionMapping[i] = multiToggleSettingIds
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
positionMapping
|
positionMapping
|
||||||
}
|
}
|
||||||
|
@@ -1,275 +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.domain.interactor
|
|
||||||
|
|
||||||
import android.bluetooth.BluetoothDevice
|
|
||||||
import android.bluetooth.BluetoothProfile
|
|
||||||
import android.content.Context
|
|
||||||
import android.media.AudioDeviceAttributes
|
|
||||||
import android.media.AudioDeviceInfo
|
|
||||||
import android.media.AudioManager
|
|
||||||
import androidx.test.core.app.ApplicationProvider
|
|
||||||
import com.android.settingslib.bluetooth.CachedBluetoothDevice
|
|
||||||
import com.android.settingslib.bluetooth.LeAudioProfile
|
|
||||||
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingModel
|
|
||||||
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingStateModel
|
|
||||||
import com.android.settingslib.media.data.repository.SpatializerRepository
|
|
||||||
import com.android.settingslib.media.domain.interactor.SpatializerInteractor
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
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.Mock
|
|
||||||
import org.mockito.Mockito.spy
|
|
||||||
import org.mockito.Mockito.times
|
|
||||||
import org.mockito.Mockito.verify
|
|
||||||
import org.mockito.Mockito.verifyNoInteractions
|
|
||||||
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 SpatialAudioInteractorTest {
|
|
||||||
@get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
|
|
||||||
|
|
||||||
@Mock private lateinit var audioManager: AudioManager
|
|
||||||
@Mock private lateinit var cachedDevice: CachedBluetoothDevice
|
|
||||||
@Mock private lateinit var bluetoothDevice: BluetoothDevice
|
|
||||||
@Mock private lateinit var spatializerRepository: SpatializerRepository
|
|
||||||
@Mock private lateinit var leAudioProfile: LeAudioProfile
|
|
||||||
|
|
||||||
private lateinit var underTest: SpatialAudioInteractor
|
|
||||||
private val testScope = TestScope()
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun setUp() {
|
|
||||||
val context = spy(ApplicationProvider.getApplicationContext<Context>())
|
|
||||||
`when`(cachedDevice.device).thenReturn(bluetoothDevice)
|
|
||||||
`when`(cachedDevice.address).thenReturn(BLUETOOTH_ADDRESS)
|
|
||||||
`when`(leAudioProfile.profileId).thenReturn(BluetoothProfile.LE_AUDIO)
|
|
||||||
underTest =
|
|
||||||
SpatialAudioInteractorImpl(
|
|
||||||
context,
|
|
||||||
audioManager,
|
|
||||||
SpatializerInteractor(spatializerRepository),
|
|
||||||
testScope.backgroundScope,
|
|
||||||
testScope.testScheduler)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun getDeviceSetting_noAudioProfile_returnNull() {
|
|
||||||
testScope.runTest {
|
|
||||||
`when`(cachedDevice.isConnected).thenReturn(true)
|
|
||||||
val setting = getLatestValue(underTest.getDeviceSetting(cachedDevice))
|
|
||||||
|
|
||||||
assertThat(setting).isNull()
|
|
||||||
verifyNoInteractions(spatializerRepository)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun getDeviceSetting_audioProfileNotEnabled_returnNull() {
|
|
||||||
testScope.runTest {
|
|
||||||
`when`(cachedDevice.isConnected).thenReturn(true)
|
|
||||||
`when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile))
|
|
||||||
`when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(false)
|
|
||||||
|
|
||||||
val setting = getLatestValue(underTest.getDeviceSetting(cachedDevice))
|
|
||||||
|
|
||||||
assertThat(setting).isNull()
|
|
||||||
verifyNoInteractions(spatializerRepository)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun getDeviceSetting_deviceNotConnected_returnNull() {
|
|
||||||
testScope.runTest {
|
|
||||||
`when`(cachedDevice.isConnected).thenReturn(false)
|
|
||||||
`when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile))
|
|
||||||
`when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true)
|
|
||||||
|
|
||||||
val setting = getLatestValue(underTest.getDeviceSetting(cachedDevice))
|
|
||||||
|
|
||||||
assertThat(setting).isNull()
|
|
||||||
verifyNoInteractions(spatializerRepository)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun getDeviceSetting_spatialAudioNotSupported_returnNull() {
|
|
||||||
testScope.runTest {
|
|
||||||
`when`(cachedDevice.isConnected).thenReturn(true)
|
|
||||||
`when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile))
|
|
||||||
`when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true)
|
|
||||||
`when`(
|
|
||||||
spatializerRepository.isSpatialAudioAvailableForDevice(
|
|
||||||
BLE_AUDIO_DEVICE_ATTRIBUTES))
|
|
||||||
.thenReturn(false)
|
|
||||||
|
|
||||||
val setting = getLatestValue(underTest.getDeviceSetting(cachedDevice))
|
|
||||||
|
|
||||||
assertThat(setting).isNull()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun getDeviceSetting_spatialAudioSupported_returnTwoToggles() {
|
|
||||||
testScope.runTest {
|
|
||||||
`when`(cachedDevice.isConnected).thenReturn(true)
|
|
||||||
`when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile))
|
|
||||||
`when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true)
|
|
||||||
`when`(
|
|
||||||
spatializerRepository.isSpatialAudioAvailableForDevice(
|
|
||||||
BLE_AUDIO_DEVICE_ATTRIBUTES))
|
|
||||||
.thenReturn(true)
|
|
||||||
`when`(
|
|
||||||
spatializerRepository.isHeadTrackingAvailableForDevice(
|
|
||||||
BLE_AUDIO_DEVICE_ATTRIBUTES))
|
|
||||||
.thenReturn(false)
|
|
||||||
`when`(spatializerRepository.getSpatialAudioCompatibleDevices())
|
|
||||||
.thenReturn(listOf(BLE_AUDIO_DEVICE_ATTRIBUTES))
|
|
||||||
`when`(spatializerRepository.isHeadTrackingEnabled(BLE_AUDIO_DEVICE_ATTRIBUTES))
|
|
||||||
.thenReturn(false)
|
|
||||||
|
|
||||||
val setting =
|
|
||||||
getLatestValue(underTest.getDeviceSetting(cachedDevice))
|
|
||||||
as DeviceSettingModel.MultiTogglePreference
|
|
||||||
|
|
||||||
assertThat(setting).isNotNull()
|
|
||||||
assertThat(setting.toggles.size).isEqualTo(2)
|
|
||||||
assertThat(setting.state.selectedIndex).isEqualTo(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun getDeviceSetting_headTrackingSupported_returnThreeToggles() {
|
|
||||||
testScope.runTest {
|
|
||||||
`when`(cachedDevice.isConnected).thenReturn(true)
|
|
||||||
`when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile))
|
|
||||||
`when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true)
|
|
||||||
`when`(
|
|
||||||
spatializerRepository.isSpatialAudioAvailableForDevice(
|
|
||||||
BLE_AUDIO_DEVICE_ATTRIBUTES))
|
|
||||||
.thenReturn(true)
|
|
||||||
`when`(
|
|
||||||
spatializerRepository.isHeadTrackingAvailableForDevice(
|
|
||||||
BLE_AUDIO_DEVICE_ATTRIBUTES))
|
|
||||||
.thenReturn(true)
|
|
||||||
`when`(spatializerRepository.getSpatialAudioCompatibleDevices())
|
|
||||||
.thenReturn(listOf(BLE_AUDIO_DEVICE_ATTRIBUTES))
|
|
||||||
`when`(spatializerRepository.isHeadTrackingEnabled(BLE_AUDIO_DEVICE_ATTRIBUTES))
|
|
||||||
.thenReturn(true)
|
|
||||||
|
|
||||||
val setting =
|
|
||||||
getLatestValue(underTest.getDeviceSetting(cachedDevice))
|
|
||||||
as DeviceSettingModel.MultiTogglePreference
|
|
||||||
|
|
||||||
assertThat(setting).isNotNull()
|
|
||||||
assertThat(setting.toggles.size).isEqualTo(3)
|
|
||||||
assertThat(setting.state.selectedIndex).isEqualTo(2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun getDeviceSetting_updateState_enableSpatialAudio() {
|
|
||||||
testScope.runTest {
|
|
||||||
`when`(cachedDevice.isConnected).thenReturn(true)
|
|
||||||
`when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile))
|
|
||||||
`when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true)
|
|
||||||
`when`(
|
|
||||||
spatializerRepository.isSpatialAudioAvailableForDevice(
|
|
||||||
BLE_AUDIO_DEVICE_ATTRIBUTES))
|
|
||||||
.thenReturn(true)
|
|
||||||
`when`(
|
|
||||||
spatializerRepository.isHeadTrackingAvailableForDevice(
|
|
||||||
BLE_AUDIO_DEVICE_ATTRIBUTES))
|
|
||||||
.thenReturn(true)
|
|
||||||
`when`(spatializerRepository.getSpatialAudioCompatibleDevices()).thenReturn(listOf())
|
|
||||||
`when`(spatializerRepository.isHeadTrackingEnabled(BLE_AUDIO_DEVICE_ATTRIBUTES))
|
|
||||||
.thenReturn(false)
|
|
||||||
|
|
||||||
val setting =
|
|
||||||
getLatestValue(underTest.getDeviceSetting(cachedDevice))
|
|
||||||
as DeviceSettingModel.MultiTogglePreference
|
|
||||||
setting.updateState(DeviceSettingStateModel.MultiTogglePreferenceState(2))
|
|
||||||
runCurrent()
|
|
||||||
|
|
||||||
assertThat(setting).isNotNull()
|
|
||||||
verify(spatializerRepository, times(1))
|
|
||||||
.addSpatialAudioCompatibleDevice(BLE_AUDIO_DEVICE_ATTRIBUTES)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun getDeviceSetting_updateState_enableHeadTracking() {
|
|
||||||
testScope.runTest {
|
|
||||||
`when`(cachedDevice.isConnected).thenReturn(true)
|
|
||||||
`when`(cachedDevice.profiles).thenReturn(listOf(leAudioProfile))
|
|
||||||
`when`(leAudioProfile.isEnabled(bluetoothDevice)).thenReturn(true)
|
|
||||||
`when`(
|
|
||||||
spatializerRepository.isSpatialAudioAvailableForDevice(
|
|
||||||
BLE_AUDIO_DEVICE_ATTRIBUTES))
|
|
||||||
.thenReturn(true)
|
|
||||||
`when`(
|
|
||||||
spatializerRepository.isHeadTrackingAvailableForDevice(
|
|
||||||
BLE_AUDIO_DEVICE_ATTRIBUTES))
|
|
||||||
.thenReturn(true)
|
|
||||||
`when`(spatializerRepository.getSpatialAudioCompatibleDevices()).thenReturn(listOf())
|
|
||||||
`when`(spatializerRepository.isHeadTrackingEnabled(BLE_AUDIO_DEVICE_ATTRIBUTES))
|
|
||||||
.thenReturn(false)
|
|
||||||
|
|
||||||
val setting =
|
|
||||||
getLatestValue(underTest.getDeviceSetting(cachedDevice))
|
|
||||||
as DeviceSettingModel.MultiTogglePreference
|
|
||||||
setting.updateState(DeviceSettingStateModel.MultiTogglePreferenceState(2))
|
|
||||||
runCurrent()
|
|
||||||
|
|
||||||
assertThat(setting).isNotNull()
|
|
||||||
verify(spatializerRepository, times(1))
|
|
||||||
.addSpatialAudioCompatibleDevice(BLE_AUDIO_DEVICE_ATTRIBUTES)
|
|
||||||
verify(spatializerRepository, times(1))
|
|
||||||
.setHeadTrackingEnabled(BLE_AUDIO_DEVICE_ATTRIBUTES, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getLatestValue(deviceSettingFlow: Flow<DeviceSettingModel?>): DeviceSettingModel? {
|
|
||||||
var latestValue: DeviceSettingModel? = null
|
|
||||||
deviceSettingFlow.onEach { latestValue = it }.launchIn(testScope.backgroundScope)
|
|
||||||
testScope.runCurrent()
|
|
||||||
return latestValue
|
|
||||||
}
|
|
||||||
|
|
||||||
private companion object {
|
|
||||||
const val BLUETOOTH_ADDRESS = "12:34:56:78:12:34"
|
|
||||||
val BLE_AUDIO_DEVICE_ATTRIBUTES =
|
|
||||||
AudioDeviceAttributes(
|
|
||||||
AudioDeviceAttributes.ROLE_OUTPUT,
|
|
||||||
AudioDeviceInfo.TYPE_BLE_HEADSET,
|
|
||||||
BLUETOOTH_ADDRESS,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@@ -20,14 +20,11 @@ import android.bluetooth.BluetoothAdapter
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.media.AudioManager
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.fragment.app.FragmentActivity
|
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.test.core.app.ApplicationProvider
|
import androidx.test.core.app.ApplicationProvider
|
||||||
import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractor
|
|
||||||
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.dashboard.DashboardFragment
|
import com.android.settings.dashboard.DashboardFragment
|
||||||
@@ -56,13 +53,11 @@ import org.junit.runner.RunWith
|
|||||||
import org.mockito.ArgumentMatchers.eq
|
import org.mockito.ArgumentMatchers.eq
|
||||||
import org.mockito.Mock
|
import org.mockito.Mock
|
||||||
import org.mockito.Mockito.any
|
import org.mockito.Mockito.any
|
||||||
import org.mockito.Mockito.verify
|
|
||||||
import org.mockito.Mockito.`when`
|
import org.mockito.Mockito.`when`
|
||||||
import org.mockito.junit.MockitoJUnit
|
import org.mockito.junit.MockitoJUnit
|
||||||
import org.mockito.junit.MockitoRule
|
import org.mockito.junit.MockitoRule
|
||||||
import org.robolectric.Robolectric
|
import org.robolectric.Robolectric
|
||||||
import org.robolectric.RobolectricTestRunner
|
import org.robolectric.RobolectricTestRunner
|
||||||
import org.robolectric.Shadows
|
|
||||||
import org.robolectric.shadows.ShadowLooper
|
import org.robolectric.shadows.ShadowLooper
|
||||||
import org.robolectric.shadows.ShadowLooper.shadowMainLooper
|
import org.robolectric.shadows.ShadowLooper.shadowMainLooper
|
||||||
|
|
||||||
@@ -74,7 +69,6 @@ class DeviceDetailsFragmentFormatterTest {
|
|||||||
@Mock private lateinit var cachedDevice: CachedBluetoothDevice
|
@Mock private lateinit var cachedDevice: CachedBluetoothDevice
|
||||||
@Mock private lateinit var bluetoothAdapter: BluetoothAdapter
|
@Mock private lateinit var bluetoothAdapter: BluetoothAdapter
|
||||||
@Mock private lateinit var repository: DeviceSettingRepository
|
@Mock private lateinit var repository: DeviceSettingRepository
|
||||||
@Mock private lateinit var spatialAudioInteractor: SpatialAudioInteractor
|
|
||||||
|
|
||||||
private lateinit var fragment: TestFragment
|
private lateinit var fragment: TestFragment
|
||||||
private lateinit var underTest: DeviceDetailsFragmentFormatter
|
private lateinit var underTest: DeviceDetailsFragmentFormatter
|
||||||
@@ -90,10 +84,6 @@ class DeviceDetailsFragmentFormatterTest {
|
|||||||
featureFactory.bluetoothFeatureProvider.getDeviceSettingRepository(
|
featureFactory.bluetoothFeatureProvider.getDeviceSettingRepository(
|
||||||
eq(context), eq(bluetoothAdapter), any()))
|
eq(context), eq(bluetoothAdapter), any()))
|
||||||
.thenReturn(repository)
|
.thenReturn(repository)
|
||||||
`when`(
|
|
||||||
featureFactory.bluetoothFeatureProvider.getSpatialAudioInteractor(
|
|
||||||
eq(context), any(AudioManager::class.java), any()))
|
|
||||||
.thenReturn(spatialAudioInteractor)
|
|
||||||
fragmentActivity = Robolectric.setupActivity(FragmentActivity::class.java)
|
fragmentActivity = Robolectric.setupActivity(FragmentActivity::class.java)
|
||||||
assertThat(fragmentActivity.applicationContext).isNotNull()
|
assertThat(fragmentActivity.applicationContext).isNotNull()
|
||||||
fragment = TestFragment(context)
|
fragment = TestFragment(context)
|
||||||
|
@@ -19,9 +19,7 @@ package com.android.settings.bluetooth.ui.viewmodel
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.bluetooth.BluetoothAdapter
|
import android.bluetooth.BluetoothAdapter
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.media.AudioManager
|
|
||||||
import androidx.test.core.app.ApplicationProvider
|
import androidx.test.core.app.ApplicationProvider
|
||||||
import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractor
|
|
||||||
import com.android.settings.bluetooth.ui.layout.DeviceSettingLayout
|
import com.android.settings.bluetooth.ui.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
|
||||||
@@ -68,8 +66,6 @@ class BluetoothDeviceDetailsViewModelTest {
|
|||||||
|
|
||||||
@Mock private lateinit var repository: DeviceSettingRepository
|
@Mock private lateinit var repository: DeviceSettingRepository
|
||||||
|
|
||||||
@Mock private lateinit var spatialAudioInteractor: SpatialAudioInteractor
|
|
||||||
|
|
||||||
private lateinit var underTest: BluetoothDeviceDetailsViewModel
|
private lateinit var underTest: BluetoothDeviceDetailsViewModel
|
||||||
private lateinit var featureFactory: FakeFeatureFactory
|
private lateinit var featureFactory: FakeFeatureFactory
|
||||||
private val testScope = TestScope()
|
private val testScope = TestScope()
|
||||||
@@ -84,11 +80,6 @@ class BluetoothDeviceDetailsViewModelTest {
|
|||||||
eq(application), eq(bluetoothAdapter), any()
|
eq(application), eq(bluetoothAdapter), any()
|
||||||
))
|
))
|
||||||
.thenReturn(repository)
|
.thenReturn(repository)
|
||||||
`when`(
|
|
||||||
featureFactory.bluetoothFeatureProvider.getSpatialAudioInteractor(
|
|
||||||
eq(application), any(AudioManager::class.java), any()
|
|
||||||
))
|
|
||||||
.thenReturn(spatialAudioInteractor)
|
|
||||||
|
|
||||||
underTest =
|
underTest =
|
||||||
BluetoothDeviceDetailsViewModel(
|
BluetoothDeviceDetailsViewModel(
|
||||||
@@ -173,37 +164,6 @@ class BluetoothDeviceDetailsViewModelTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun getDeviceSetting_spatialAudio_returnSpatialAudioInteractorResponse() {
|
|
||||||
testScope.runTest {
|
|
||||||
val pref =
|
|
||||||
buildMultiTogglePreference(
|
|
||||||
DeviceSettingId.DEVICE_SETTING_ID_SPATIAL_AUDIO_MULTI_TOGGLE)
|
|
||||||
`when`(repository.getDeviceSettingsConfig(cachedDevice))
|
|
||||||
.thenReturn(
|
|
||||||
DeviceSettingConfigModel(
|
|
||||||
listOf(
|
|
||||||
BUILTIN_SETTING_ITEM_1,
|
|
||||||
buildRemoteSettingItem(
|
|
||||||
DeviceSettingId.DEVICE_SETTING_ID_SPATIAL_AUDIO_MULTI_TOGGLE),
|
|
||||||
),
|
|
||||||
listOf(),
|
|
||||||
null))
|
|
||||||
`when`(spatialAudioInteractor.getDeviceSetting(cachedDevice)).thenReturn(flowOf(pref))
|
|
||||||
|
|
||||||
var deviceSettingPreference: DeviceSettingPreferenceModel? = null
|
|
||||||
underTest
|
|
||||||
.getDeviceSetting(
|
|
||||||
cachedDevice, DeviceSettingId.DEVICE_SETTING_ID_SPATIAL_AUDIO_MULTI_TOGGLE)
|
|
||||||
.onEach { deviceSettingPreference = it }
|
|
||||||
.launchIn(testScope.backgroundScope)
|
|
||||||
runCurrent()
|
|
||||||
|
|
||||||
assertThat(deviceSettingPreference?.id).isEqualTo(pref.id)
|
|
||||||
verify(spatialAudioInteractor, times(1)).getDeviceSetting(cachedDevice)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun getLayout_builtinDeviceSettings() {
|
fun getLayout_builtinDeviceSettings() {
|
||||||
testScope.runTest {
|
testScope.runTest {
|
||||||
@@ -252,7 +212,8 @@ class BluetoothDeviceDetailsViewModelTest {
|
|||||||
.isEqualTo(
|
.isEqualTo(
|
||||||
listOf(
|
listOf(
|
||||||
listOf(DeviceSettingId.DEVICE_SETTING_ID_HEADER),
|
listOf(DeviceSettingId.DEVICE_SETTING_ID_HEADER),
|
||||||
listOf(remoteSettingId1, remoteSettingId2),
|
listOf(remoteSettingId1),
|
||||||
|
listOf(remoteSettingId2),
|
||||||
listOf(remoteSettingId3),
|
listOf(remoteSettingId3),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user