1 /* <lambda>null2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.bluetooth.qsdialog 18 19 import android.bluetooth.BluetoothAdapter 20 import android.bluetooth.BluetoothDevice 21 import android.content.Context 22 import android.media.AudioManager 23 import com.android.settingslib.bluetooth.BluetoothCallback 24 import com.android.settingslib.bluetooth.CachedBluetoothDevice 25 import com.android.settingslib.bluetooth.LocalBluetoothManager 26 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging 27 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow 28 import com.android.systemui.dagger.SysUISingleton 29 import com.android.systemui.dagger.qualifiers.Application 30 import com.android.systemui.dagger.qualifiers.Background 31 import com.android.systemui.util.time.SystemClock 32 import javax.inject.Inject 33 import kotlinx.coroutines.CoroutineDispatcher 34 import kotlinx.coroutines.CoroutineScope 35 import kotlinx.coroutines.channels.awaitClose 36 import kotlinx.coroutines.flow.MutableSharedFlow 37 import kotlinx.coroutines.flow.SharedFlow 38 import kotlinx.coroutines.flow.SharingStarted 39 import kotlinx.coroutines.flow.asSharedFlow 40 import kotlinx.coroutines.flow.shareIn 41 import kotlinx.coroutines.isActive 42 import kotlinx.coroutines.withContext 43 44 /** Holds business logic for the Bluetooth Dialog after clicking on the Bluetooth QS tile. */ 45 @SysUISingleton 46 internal class DeviceItemInteractor 47 @Inject 48 constructor( 49 private val bluetoothTileDialogRepository: BluetoothTileDialogRepository, 50 private val audioManager: AudioManager, 51 private val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter(), 52 private val localBluetoothManager: LocalBluetoothManager?, 53 private val systemClock: SystemClock, 54 private val logger: BluetoothTileDialogLogger, 55 @Application private val coroutineScope: CoroutineScope, 56 @Background private val backgroundDispatcher: CoroutineDispatcher, 57 ) { 58 59 private val mutableDeviceItemUpdate: MutableSharedFlow<List<DeviceItem>> = 60 MutableSharedFlow(extraBufferCapacity = 1) 61 internal val deviceItemUpdate 62 get() = mutableDeviceItemUpdate.asSharedFlow() 63 64 internal val deviceItemUpdateRequest: SharedFlow<Unit> = 65 conflatedCallbackFlow { 66 val listener = 67 object : BluetoothCallback { 68 override fun onActiveDeviceChanged( 69 activeDevice: CachedBluetoothDevice?, 70 bluetoothProfile: Int 71 ) { 72 super.onActiveDeviceChanged(activeDevice, bluetoothProfile) 73 logger.logActiveDeviceChanged(activeDevice?.address, bluetoothProfile) 74 trySendWithFailureLogging(Unit, TAG, "onActiveDeviceChanged") 75 } 76 77 override fun onProfileConnectionStateChanged( 78 cachedDevice: CachedBluetoothDevice, 79 state: Int, 80 bluetoothProfile: Int 81 ) { 82 super.onProfileConnectionStateChanged( 83 cachedDevice, 84 state, 85 bluetoothProfile 86 ) 87 logger.logProfileConnectionStateChanged( 88 cachedDevice.address, 89 state.toString(), 90 bluetoothProfile 91 ) 92 trySendWithFailureLogging(Unit, TAG, "onProfileConnectionStateChanged") 93 } 94 95 override fun onAclConnectionStateChanged( 96 cachedDevice: CachedBluetoothDevice, 97 state: Int 98 ) { 99 super.onAclConnectionStateChanged(cachedDevice, state) 100 // Listen only when a device is disconnecting 101 if (state == 0) { 102 trySendWithFailureLogging(Unit, TAG, "onAclConnectionStateChanged") 103 } 104 } 105 } 106 localBluetoothManager?.eventManager?.registerCallback(listener) 107 awaitClose { localBluetoothManager?.eventManager?.unregisterCallback(listener) } 108 } 109 .shareIn(coroutineScope, SharingStarted.WhileSubscribed(replayExpirationMillis = 0)) 110 111 private var deviceItemFactoryList: List<DeviceItemFactory> = 112 listOf( 113 ActiveMediaDeviceItemFactory(), 114 AudioSharingMediaDeviceItemFactory(localBluetoothManager), 115 AvailableMediaDeviceItemFactory(), 116 ConnectedDeviceItemFactory(), 117 SavedDeviceItemFactory() 118 ) 119 120 private var displayPriority: List<DeviceItemType> = 121 listOf( 122 DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE, 123 DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE, 124 DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE, 125 DeviceItemType.CONNECTED_BLUETOOTH_DEVICE, 126 DeviceItemType.SAVED_BLUETOOTH_DEVICE, 127 ) 128 129 internal suspend fun updateDeviceItems(context: Context, trigger: DeviceFetchTrigger) { 130 withContext(backgroundDispatcher) { 131 val start = systemClock.elapsedRealtime() 132 val deviceItems = 133 bluetoothTileDialogRepository.cachedDevices 134 .mapNotNull { cachedDevice -> 135 deviceItemFactoryList 136 .firstOrNull { it.isFilterMatched(context, cachedDevice, audioManager) } 137 ?.create(context, cachedDevice) 138 } 139 .sort(displayPriority, bluetoothAdapter?.mostRecentlyConnectedDevices) 140 // Only emit when the job is not cancelled 141 if (isActive) { 142 mutableDeviceItemUpdate.tryEmit(deviceItems) 143 logger.logDeviceFetch( 144 JobStatus.FINISHED, 145 trigger, 146 systemClock.elapsedRealtime() - start 147 ) 148 } else { 149 logger.logDeviceFetch( 150 JobStatus.CANCELLED, 151 trigger, 152 systemClock.elapsedRealtime() - start 153 ) 154 } 155 } 156 } 157 158 private fun List<DeviceItem>.sort( 159 displayPriority: List<DeviceItemType>, 160 mostRecentlyConnectedDevices: List<BluetoothDevice>? 161 ): List<DeviceItem> { 162 return this.sortedWith( 163 compareBy<DeviceItem> { displayPriority.indexOf(it.type) } 164 .thenBy { 165 mostRecentlyConnectedDevices?.indexOf(it.cachedBluetoothDevice.device) ?: 0 166 } 167 ) 168 } 169 170 internal fun setDeviceItemFactoryListForTesting(list: List<DeviceItemFactory>) { 171 deviceItemFactoryList = list 172 } 173 174 internal fun setDisplayPriorityForTesting(list: List<DeviceItemType>) { 175 displayPriority = list 176 } 177 178 companion object { 179 private const val TAG = "DeviceItemInteractor" 180 } 181 } 182