/* * 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.systemui.bluetooth.qsdialog import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.settingslib.bluetooth.onServiceStateChanged import com.android.settingslib.bluetooth.onSourceConnectedOrRemoved import com.android.settingslib.volume.data.repository.AudioSharingRepository as SettingsLibAudioSharingRepository import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext interface AudioSharingRepository { val isAudioSharingProfilesReady: StateFlow val leAudioBroadcastProfile: LocalBluetoothLeBroadcast? val audioSourceStateUpdate: Flow val inAudioSharing: StateFlow suspend fun audioSharingAvailable(): Boolean suspend fun addSource() suspend fun setActive(cachedBluetoothDevice: CachedBluetoothDevice) suspend fun startAudioSharing() suspend fun stopAudioSharing() } @SysUISingleton class AudioSharingRepositoryImpl( private val localBluetoothManager: LocalBluetoothManager, private val settingsLibAudioSharingRepository: SettingsLibAudioSharingRepository, private val logger: BluetoothTileDialogLogger, @Background private val backgroundDispatcher: CoroutineDispatcher, @Application private val coroutineScope: CoroutineScope, ) : AudioSharingRepository { override val leAudioBroadcastProfile: LocalBluetoothLeBroadcast? get() = localBluetoothManager.profileManager.leAudioBroadcastProfile private val leAudioBroadcastAssistantProfile: LocalBluetoothLeBroadcastAssistant? get() = localBluetoothManager.profileManager.leAudioBroadcastAssistantProfile override val isAudioSharingProfilesReady: StateFlow = localBluetoothManager.profileManager.onServiceStateChanged .map { leAudioBroadcastProfile?.isProfileReady == true && leAudioBroadcastAssistantProfile?.isProfileReady == true } .onStart { emit( leAudioBroadcastProfile?.isProfileReady == true && leAudioBroadcastAssistantProfile?.isProfileReady == true ) } .flowOn(backgroundDispatcher) .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), false) override val audioSourceStateUpdate: Flow = isAudioSharingProfilesReady .flatMapLatest { if (it) { leAudioBroadcastAssistantProfile?.onSourceConnectedOrRemoved ?: emptyFlow() } else { emptyFlow() } } .flowOn(backgroundDispatcher) override val inAudioSharing: StateFlow = settingsLibAudioSharingRepository.inAudioSharing override suspend fun audioSharingAvailable(): Boolean { return settingsLibAudioSharingRepository.audioSharingAvailable() } override suspend fun addSource() { withContext(backgroundDispatcher) { if (!settingsLibAudioSharingRepository.audioSharingAvailable()) { return@withContext } leAudioBroadcastProfile?.latestBluetoothLeBroadcastMetadata?.let { metadata -> leAudioBroadcastAssistantProfile?.let { it.allConnectedDevices.forEach { sink -> it.addSource(sink, metadata, false).also { logger.logAudioSharingRequest(AudioSharingRequest.ADD_SOURCE) } } } } } } override suspend fun setActive(cachedBluetoothDevice: CachedBluetoothDevice) { withContext(backgroundDispatcher) { if (!settingsLibAudioSharingRepository.audioSharingAvailable()) { return@withContext } cachedBluetoothDevice.setActive() } } override suspend fun startAudioSharing() { withContext(backgroundDispatcher) { if (!settingsLibAudioSharingRepository.audioSharingAvailable()) { return@withContext } leAudioBroadcastProfile?.startPrivateBroadcast().also { logger.logAudioSharingRequest(AudioSharingRequest.START_BROADCAST) } } } override suspend fun stopAudioSharing() { withContext(backgroundDispatcher) { if (!settingsLibAudioSharingRepository.audioSharingAvailable()) { return@withContext } leAudioBroadcastProfile?.stopLatestBroadcast().also { logger.logAudioSharingRequest(AudioSharingRequest.STOP_BROADCAST) } } } } @SysUISingleton class AudioSharingRepositoryEmptyImpl : AudioSharingRepository { override val isAudioSharingProfilesReady: StateFlow = MutableStateFlow(false) override val leAudioBroadcastProfile: LocalBluetoothLeBroadcast? = null override val audioSourceStateUpdate: Flow = emptyFlow() override val inAudioSharing: StateFlow = MutableStateFlow(false) override suspend fun audioSharingAvailable(): Boolean = false override suspend fun addSource() {} override suspend fun setActive(cachedBluetoothDevice: CachedBluetoothDevice) {} override suspend fun startAudioSharing() {} override suspend fun stopAudioSharing() {} }