Implement Spatial audio toggle domain layer
BUG: 343317785 Test: atest SpatialAudioInteractorTest Flag: com.android.settings.flags.enable_bluetooth_device_details_polish Change-Id: Ic73e56a1ca41f9fa58d5219666478a7edc55059d
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
48
src/com/android/settings/bluetooth/ui/composable/Icon.kt
Normal file
48
src/com/android/settings/bluetooth/ui/composable/Icon.kt
Normal 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 -> {}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user