Merge "Implement Spatial audio toggle domain layer" into main

This commit is contained in:
Haijie Hong
2024-08-15 10:54:48 +00:00
committed by Android (Google) Code Review
17 changed files with 825 additions and 257 deletions

View File

@@ -39,8 +39,8 @@ import androidx.preference.TwoStatePreference;
import com.android.settings.R;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.bluetooth.BluetoothUtils;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.LocalBluetoothProfile;
import com.android.settingslib.core.lifecycle.Lifecycle;
import com.android.settingslib.flags.Flags;
import com.android.settingslib.utils.ThreadUtils;
@@ -299,57 +299,14 @@ public class BluetoothDetailsSpatialAudioController extends BluetoothDetailsCont
+ " profiles: "
+ mCachedDevice.getProfiles());
AudioDeviceAttributes saDevice = null;
for (LocalBluetoothProfile profile : mCachedDevice.getProfiles()) {
// pick first enabled profile that is compatible with spatial audio
if (SA_PROFILES.contains(profile.getProfileId())
&& profile.isEnabled(mCachedDevice.getDevice())) {
switch (profile.getProfileId()) {
case BluetoothProfile.A2DP:
saDevice =
new AudioDeviceAttributes(
AudioDeviceAttributes.ROLE_OUTPUT,
AudioDeviceInfo.TYPE_BLUETOOTH_A2DP,
mCachedDevice.getAddress());
break;
case BluetoothProfile.LE_AUDIO:
if (mAudioManager.getBluetoothAudioDeviceCategory(
mCachedDevice.getAddress())
== AudioManager.AUDIO_DEVICE_CATEGORY_SPEAKER) {
saDevice =
new AudioDeviceAttributes(
AudioDeviceAttributes.ROLE_OUTPUT,
AudioDeviceInfo.TYPE_BLE_SPEAKER,
mCachedDevice.getAddress());
} else {
saDevice =
new AudioDeviceAttributes(
AudioDeviceAttributes.ROLE_OUTPUT,
AudioDeviceInfo.TYPE_BLE_HEADSET,
mCachedDevice.getAddress());
}
break;
case BluetoothProfile.HEARING_AID:
saDevice =
new AudioDeviceAttributes(
AudioDeviceAttributes.ROLE_OUTPUT,
AudioDeviceInfo.TYPE_HEARING_AID,
mCachedDevice.getAddress());
break;
default:
Log.i(
TAG,
"unrecognized profile for spatial audio: "
+ profile.getProfileId());
break;
}
break;
}
}
mAudioDevice = null;
AudioDeviceAttributes saDevice =
BluetoothUtils.getAudioDeviceAttributesForSpatialAudio(
mCachedDevice,
mAudioManager.getBluetoothAudioDeviceCategory(mCachedDevice.getAddress()));
if (saDevice != null && mSpatializer.isAvailableForDevice(saDevice)) {
mAudioDevice = saDevice;
} else {
mAudioDevice = null;
}
Log.d(

View File

@@ -20,6 +20,7 @@ import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.ComponentName;
import android.content.Context;
import android.media.AudioManager;
import android.media.Spatializer;
import android.net.Uri;
@@ -28,6 +29,7 @@ import androidx.lifecycle.LifecycleCoroutineScope;
import androidx.preference.Preference;
import com.android.settings.SettingsPreferenceFragment;
import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractor;
import com.android.settings.bluetooth.ui.view.DeviceDetailsFragmentFormatter;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository;
@@ -98,6 +100,13 @@ public interface BluetoothFeatureProvider {
@NonNull BluetoothAdapter bluetoothAdapter,
@NonNull LifecycleCoroutineScope scope);
/** Gets spatial audio interactor. */
@NonNull
SpatialAudioInteractor getSpatialAudioInteractor(
@NonNull Context context,
@NonNull AudioManager audioManager,
@NonNull LifecycleCoroutineScope scope);
/** Gets device details fragment layout formatter. */
@NonNull
DeviceDetailsFragmentFormatter getDeviceDetailsFragmentFormatter(

View File

@@ -1,105 +0,0 @@
/*
* Copyright (C) 2018 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;
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;
import android.media.AudioManager;
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;
import java.util.List;
import java.util.Set;
/**
* Impl of {@link BluetoothFeatureProvider}
*/
public class BluetoothFeatureProviderImpl implements BluetoothFeatureProvider {
@Override
public Uri getBluetoothDeviceSettingsUri(BluetoothDevice bluetoothDevice) {
final byte[] uriByte = bluetoothDevice.getMetadata(
BluetoothDevice.METADATA_ENHANCED_SETTINGS_UI_URI);
return uriByte == null ? null : Uri.parse(new String(uriByte));
}
@Override
public String getBluetoothDeviceControlUri(BluetoothDevice bluetoothDevice) {
return BluetoothUtils.getControlUriMetaData(bluetoothDevice);
}
@Override
public List<ComponentName> getRelatedTools() {
return null;
}
@Override
public Spatializer getSpatializer(Context context) {
AudioManager audioManager = context.getSystemService(AudioManager.class);
return audioManager.getSpatializer();
}
@Override
public List<Preference> getBluetoothExtraOptions(Context context,
CachedBluetoothDevice device) {
return ImmutableList.of();
}
@Override
public Set<String> getInvisibleProfilePreferenceKeys(
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);
}
}

View File

@@ -0,0 +1,106 @@
/*
* Copyright (C) 2018 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
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.content.ComponentName
import android.content.Context
import android.media.AudioManager
import android.media.Spatializer
import android.net.Uri
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.preference.Preference
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.DeviceDetailsFragmentFormatterImpl
import com.android.settingslib.bluetooth.BluetoothUtils
import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.bluetooth.devicesettings.data.repository.DeviceSettingRepository
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.ImmutableSet
import kotlinx.coroutines.Dispatchers
/** Impl of [BluetoothFeatureProvider] */
open class BluetoothFeatureProviderImpl : BluetoothFeatureProvider {
override fun getBluetoothDeviceSettingsUri(bluetoothDevice: BluetoothDevice): Uri? {
val uriByte = bluetoothDevice.getMetadata(BluetoothDevice.METADATA_ENHANCED_SETTINGS_UI_URI)
return uriByte?.let { Uri.parse(String(it)) }
}
override fun getBluetoothDeviceControlUri(bluetoothDevice: BluetoothDevice): String? {
return BluetoothUtils.getControlUriMetaData(bluetoothDevice)
}
override fun getRelatedTools(): List<ComponentName>? {
return null
}
override fun getSpatializer(context: Context): Spatializer? {
val audioManager = context.getSystemService(AudioManager::class.java)
return audioManager.spatializer
}
override fun getBluetoothExtraOptions(
context: Context,
device: CachedBluetoothDevice
): List<Preference>? {
return ImmutableList.of<Preference>()
}
override fun getInvisibleProfilePreferenceKeys(
context: Context,
bluetoothDevice: BluetoothDevice
): Set<String> {
return ImmutableSet.of()
}
override fun getDeviceSettingRepository(
context: Context,
bluetoothAdapter: BluetoothAdapter,
scope: LifecycleCoroutineScope
): DeviceSettingRepository =
DeviceSettingRepositoryImpl(context, bluetoothAdapter, scope, Dispatchers.IO)
override fun getSpatialAudioInteractor(
context: Context,
audioManager: AudioManager,
scope: LifecycleCoroutineScope
): SpatialAudioInteractor {
return SpatialAudioInteractorImpl(
context, audioManager,
SpatializerInteractor(
SpatializerRepositoryImpl(
audioManager.spatializer,
Dispatchers.IO
)
), scope, Dispatchers.IO)
}
override fun getDeviceDetailsFragmentFormatter(
context: Context,
fragment: SettingsPreferenceFragment,
bluetoothAdapter: BluetoothAdapter,
cachedDevice: CachedBluetoothDevice
): DeviceDetailsFragmentFormatter {
return DeviceDetailsFragmentFormatterImpl(context, fragment, bluetoothAdapter, cachedDevice)
}
}

View File

@@ -0,0 +1,155 @@
/*
* 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.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
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) }
.map { getSpatialAudioDeviceSettingModel(cachedDevice) }
.stateIn(coroutineScope, SharingStarted.WhileSubscribed(), initialValue = null)
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 = "SpatialAudioInteractorImpl"
private const val INDEX_SPATIAL_AUDIO_OFF = 0
private const val INDEX_SPATIAL_AUDIO_ON = 1
private const val INDEX_HEAD_TRACKING_ENABLED = 2
}
}

View File

@@ -0,0 +1,48 @@
/*
* 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.material3.LocalContentColor
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.res.painterResource
import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSettingIcon
@Composable
fun Icon(
icon: DeviceSettingIcon,
modifier: Modifier = Modifier,
tint: Color = LocalContentColor.current,
) {
when (icon) {
is DeviceSettingIcon.BitmapIcon ->
androidx.compose.material3.Icon(
icon.bitmap.asImageBitmap(),
contentDescription = null,
modifier = modifier,
tint = LocalContentColor.current)
is DeviceSettingIcon.ResourceIcon ->
androidx.compose.material3.Icon(
painterResource(icon.resId),
contentDescription = null,
modifier = modifier,
tint = tint)
else -> {}
}
}

View File

@@ -51,7 +51,6 @@ 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.graphics.asImageBitmap
import androidx.compose.ui.layout.boundsInParent
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
@@ -67,6 +66,7 @@ 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.composable.Icon as DeviceSettingComposeIcon
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
@@ -97,35 +97,29 @@ fun MultiTogglePreferenceGroup(
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.isActive) {
ToggleableState.On
} else {
ToggleableState.Off
}
contentDescription = preferenceModel.title
},
onClick = { settingIdForPopUp = preferenceModel.id },
shape = RoundedCornerShape(20.dp),
colors = getButtonColors(preferenceModel.isActive),
contentPadding = PaddingValues(0.dp)
) {
Icon(
preferenceModel.toggles[preferenceModel.state.selectedIndex]
.icon
.asImageBitmap(),
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = LocalContentColor.current
)
color = MaterialTheme.colorScheme.surface) {
Button(
modifier =
Modifier.fillMaxSize().padding(8.dp).semantics {
role = Role.Switch
toggleableState =
if (preferenceModel.isActive) {
ToggleableState.On
} else {
ToggleableState.Off
}
contentDescription = preferenceModel.title
},
onClick = { settingIdForPopUp = preferenceModel.id },
shape = RoundedCornerShape(20.dp),
colors = getButtonColors(preferenceModel.isActive),
contentPadding = PaddingValues(0.dp)) {
DeviceSettingComposeIcon(
preferenceModel.toggles[preferenceModel.state.selectedIndex]
.icon,
modifier = Modifier.size(24.dp))
}
}
}
}
Row { Text(text = preferenceModel.title, fontSize = 12.sp) }
}
@@ -173,8 +167,7 @@ private fun dialog(
Icon(
painterResource(id = R.drawable.ic_close),
null,
tint = MaterialTheme.colorScheme.inverseSurface
)
tint = MaterialTheme.colorScheme.inverseSurface)
}
Box(modifier = Modifier.padding(horizontal = 8.dp, vertical = 20.dp)) {
dialogContent(multiTogglePreference)
@@ -182,8 +175,7 @@ private fun dialog(
}
},
)
}
)
})
}
@Composable
@@ -208,9 +200,7 @@ private fun dialogContent(multiTogglePreference: DeviceSettingModel.MultiToggleP
Modifier.fillMaxWidth()
.height(64.dp)
.background(
MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(28.dp)
),
MaterialTheme.colorScheme.surface, shape = RoundedCornerShape(28.dp)),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly,
) {
@@ -224,9 +214,7 @@ private fun dialogContent(multiTogglePreference: DeviceSettingModel.MultiToggleP
.width(selectedRect!!.width.toDp())
.background(
MaterialTheme.colorScheme.tertiaryContainer,
shape = RoundedCornerShape(20.dp)
)
)
shape = RoundedCornerShape(20.dp)))
}
}
Row {
@@ -238,9 +226,7 @@ private fun dialogContent(multiTogglePreference: DeviceSettingModel.MultiToggleP
.padding(horizontal = 8.dp)
.height(48.dp)
.background(
Color.Transparent,
shape = RoundedCornerShape(28.dp)
)
Color.Transparent, shape = RoundedCornerShape(28.dp))
.onGloballyPositioned { layoutCoordinates ->
if (selected) {
selectedRect = layoutCoordinates.boundsInParent()
@@ -252,22 +238,16 @@ private fun dialogContent(multiTogglePreference: DeviceSettingModel.MultiToggleP
Button(
onClick = {
multiTogglePreference.updateState(
DeviceSettingStateModel.MultiTogglePreferenceState(idx)
)
DeviceSettingStateModel.MultiTogglePreferenceState(idx))
},
modifier = Modifier.fillMaxSize(),
colors =
ButtonDefaults.buttonColors(
containerColor = Color.Transparent,
contentColor = LocalContentColor.current
),
contentColor = LocalContentColor.current),
) {
Icon(
bitmap = toggle.icon.asImageBitmap(),
null,
modifier = Modifier.size(24.dp),
tint = LocalContentColor.current
)
DeviceSettingComposeIcon(
toggle.icon, modifier = Modifier.size(24.dp))
}
}
}
@@ -285,8 +265,7 @@ private fun dialogContent(multiTogglePreference: DeviceSettingModel.MultiToggleP
text = toggle.label,
fontSize = 12.sp,
textAlign = TextAlign.Center,
modifier = Modifier.weight(1f).padding(horizontal = 8.dp)
)
modifier = Modifier.weight(1f).padding(horizontal = 8.dp))
}
}
}

View File

@@ -18,20 +18,19 @@ package com.android.settings.bluetooth.ui.view
import android.bluetooth.BluetoothAdapter
import android.content.Context
import android.media.AudioManager
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.Icon
import com.android.settings.bluetooth.ui.composable.MultiTogglePreferenceGroup
import com.android.settings.bluetooth.ui.layout.DeviceSettingLayout
import com.android.settings.bluetooth.ui.viewmodel.BluetoothDeviceDetailsViewModel
@@ -42,7 +41,6 @@ import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSetti
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
@@ -52,6 +50,8 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.runBlocking
import com.android.settingslib.spa.widget.preference.Preference as SpaPreference
/** Handles device details fragment layout according to config. */
interface DeviceDetailsFragmentFormatter {
@@ -72,19 +72,24 @@ class DeviceDetailsFragmentFormatterImpl(
private val repository =
featureFactory.bluetoothFeatureProvider.getDeviceSettingRepository(
context, bluetoothAdapter, fragment.lifecycleScope)
private val spatialAudioInteractor =
featureFactory.bluetoothFeatureProvider.getSpatialAudioInteractor(
context, context.getSystemService(AudioManager::class.java), fragment.lifecycleScope)
private val viewModel: BluetoothDeviceDetailsViewModel =
ViewModelProvider(
fragment,
BluetoothDeviceDetailsViewModel.Factory(
repository,
spatialAudioInteractor,
cachedDevice,
))
.get(BluetoothDeviceDetailsViewModel::class.java)
override fun getVisiblePreferenceKeysForMainPage(): List<String>? = runBlocking {
viewModel.getItems()?.filterIsInstance<DeviceSettingConfigItemModel.BuiltinItem>()?.map {
it.preferenceKey
}
viewModel
.getItems()
?.filterIsInstance<DeviceSettingConfigItemModel.BuiltinItem>()
?.mapNotNull { it.preferenceKey }
}
/** Updates bluetooth device details fragment layout. */
@@ -208,12 +213,8 @@ class DeviceDetailsFragmentFormatterImpl(
@Composable
private fun deviceSettingIcon(model: DeviceSettingModel.ActionSwitchPreference) {
model.icon?.let { bitmap ->
Icon(
bitmap.asImageBitmap(),
contentDescription = null,
modifier = Modifier.size(SettingsDimension.itemIconSize),
tint = LocalContentColor.current)
model.icon?.let { icon ->
Icon(icon, modifier = Modifier.size(SettingsDimension.itemIconSize))
}
}

View File

@@ -19,6 +19,7 @@ package com.android.settings.bluetooth.ui.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.android.settings.bluetooth.domain.interactor.SpatialAudioInteractor
import com.android.settings.bluetooth.ui.layout.DeviceSettingLayout
import com.android.settings.bluetooth.ui.layout.DeviceSettingLayoutRow
import com.android.settingslib.bluetooth.CachedBluetoothDevice
@@ -29,6 +30,7 @@ import com.android.settingslib.bluetooth.devicesettings.shared.model.DeviceSetti
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOf
@@ -37,6 +39,7 @@ import kotlinx.coroutines.flow.stateIn
class BluetoothDeviceDetailsViewModel(
private val deviceSettingRepository: DeviceSettingRepository,
private val spatialAudioInteractor: SpatialAudioInteractor,
private val cachedDevice: CachedBluetoothDevice,
) : ViewModel() {
private val items =
@@ -46,8 +49,16 @@ class BluetoothDeviceDetailsViewModel(
suspend fun getItems(): List<DeviceSettingConfigItemModel>? = items.await()?.mainItems
fun getDeviceSetting(cachedDevice: CachedBluetoothDevice, @DeviceSettingId settingId: Int) =
deviceSettingRepository.getDeviceSetting(cachedDevice, settingId)
fun getDeviceSetting(
cachedDevice: CachedBluetoothDevice,
@DeviceSettingId settingId: Int
): Flow<DeviceSettingModel?> {
return when (settingId) {
DeviceSettingId.DEVICE_SETTING_ID_SPATIAL_AUDIO_MULTI_TOGGLE ->
spatialAudioInteractor.getDeviceSetting(cachedDevice)
else -> deviceSettingRepository.getDeviceSetting(cachedDevice, settingId)
}
}
suspend fun getLayout(): DeviceSettingLayout? {
val configItems = getItems() ?: return null
@@ -93,11 +104,14 @@ class BluetoothDeviceDetailsViewModel(
class Factory(
private val deviceSettingRepository: DeviceSettingRepository,
private val spatialAudioInteractor: SpatialAudioInteractor,
private val cachedDevice: CachedBluetoothDevice,
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")
return BluetoothDeviceDetailsViewModel(deviceSettingRepository, cachedDevice) as T
return BluetoothDeviceDetailsViewModel(
deviceSettingRepository, spatialAudioInteractor, cachedDevice)
as T
}
}

View File

@@ -1,29 +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.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)