Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit aaa040e0 authored by Haijie Hong's avatar Haijie Hong
Browse files

Revert ANC and Spatial audio UI change

BUG: 378016708
Test: atest DeviceDetailsFragmentFormatterTest
Flag: com.android.settings.flags.enable_bluetooth_device_details_polish
Change-Id: I390ff06ab11b16134d0656c9576670b319c46c74
parent 2ae605a0
Loading
Loading
Loading
Loading
+0 −9
Original line number Diff line number Diff line
@@ -20,7 +20,6 @@ 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,7 +27,6 @@ import androidx.annotation.NonNull;
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,13 +96,6 @@ public interface BluetoothFeatureProvider {
            @NonNull BluetoothAdapter bluetoothAdapter,
            @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. */
    @NonNull
    DeviceDetailsFragmentFormatter getDeviceDetailsFragmentFormatter(
+0 −21
Original line number Diff line number Diff line
@@ -22,20 +22,14 @@ import android.content.Context
import android.media.AudioManager
import android.media.Spatializer
import android.net.Uri
import android.util.Log
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.CoroutineScope
@@ -82,21 +76,6 @@ open class BluetoothFeatureProviderImpl : BluetoothFeatureProvider {
    ): DeviceSettingRepository =
        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(
        context: Context,
        fragment: SettingsPreferenceFragment,
+0 −180
Original line number Diff line number Diff line
/*
 * 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
    }
}
+124 −0
Original line number Diff line number Diff line
/*
 * 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,
    )
}
+0 −280

File deleted.

Preview size limit exceeded, changes collapsed.

Loading