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.MutableStateFlow 38 import kotlinx.coroutines.flow.SharedFlow 39 import kotlinx.coroutines.flow.SharingStarted 40 import kotlinx.coroutines.flow.asSharedFlow 41 import kotlinx.coroutines.flow.asStateFlow 42 import kotlinx.coroutines.flow.flowOn 43 import kotlinx.coroutines.flow.shareIn 44 import kotlinx.coroutines.isActive 45 import kotlinx.coroutines.withContext 46 47 /** Holds business logic for the Bluetooth Dialog after clicking on the Bluetooth QS tile. */ 48 @SysUISingleton 49 class DeviceItemInteractor 50 @Inject 51 constructor( 52 private val bluetoothTileDialogRepository: BluetoothTileDialogRepository, 53 private val audioSharingInteractor: AudioSharingInteractor, 54 private val audioManager: AudioManager, 55 private val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter(), 56 private val localBluetoothManager: LocalBluetoothManager?, 57 private val systemClock: SystemClock, 58 private val logger: BluetoothTileDialogLogger, 59 private val deviceItemFactoryList: List<@JvmSuppressWildcards DeviceItemFactory>, 60 private val deviceItemDisplayPriority: List<@JvmSuppressWildcards DeviceItemType>, 61 @Application private val coroutineScope: CoroutineScope, 62 @Background private val backgroundDispatcher: CoroutineDispatcher, 63 ) { 64 65 private val mutableDeviceItemUpdate: MutableSharedFlow<List<DeviceItem>> = 66 MutableSharedFlow(extraBufferCapacity = 1) 67 val deviceItemUpdate 68 get() = mutableDeviceItemUpdate.asSharedFlow() 69 70 private val mutableShowSeeAllUpdate: MutableStateFlow<Boolean> = MutableStateFlow(false) 71 internal val showSeeAllUpdate 72 get() = mutableShowSeeAllUpdate.asStateFlow() 73 74 val deviceItemUpdateRequest: SharedFlow<Unit> = 75 conflatedCallbackFlow { 76 val listener = 77 object : BluetoothCallback { 78 override fun onActiveDeviceChanged( 79 activeDevice: CachedBluetoothDevice?, 80 bluetoothProfile: Int, 81 ) { 82 super.onActiveDeviceChanged(activeDevice, bluetoothProfile) 83 logger.logActiveDeviceChanged(activeDevice?.address, bluetoothProfile) 84 trySendWithFailureLogging(Unit, TAG, "onActiveDeviceChanged") 85 } 86 87 override fun onProfileConnectionStateChanged( 88 cachedDevice: CachedBluetoothDevice, 89 state: Int, 90 bluetoothProfile: Int, 91 ) { 92 super.onProfileConnectionStateChanged( 93 cachedDevice, 94 state, 95 bluetoothProfile, 96 ) 97 logger.logProfileConnectionStateChanged( 98 cachedDevice.address, 99 state.toString(), 100 bluetoothProfile, 101 ) 102 trySendWithFailureLogging(Unit, TAG, "onProfileConnectionStateChanged") 103 } 104 105 override fun onAclConnectionStateChanged( 106 cachedDevice: CachedBluetoothDevice, 107 state: Int, 108 ) { 109 super.onAclConnectionStateChanged(cachedDevice, state) 110 trySendWithFailureLogging(Unit, TAG, "onAclConnectionStateChanged") 111 } 112 } 113 localBluetoothManager?.eventManager?.registerCallback(listener) 114 awaitClose { localBluetoothManager?.eventManager?.unregisterCallback(listener) } 115 } 116 .flowOn(backgroundDispatcher) 117 .shareIn(coroutineScope, SharingStarted.WhileSubscribed(replayExpirationMillis = 0)) 118 119 internal suspend fun updateDeviceItems(context: Context, trigger: DeviceFetchTrigger) { 120 withContext(backgroundDispatcher) { 121 val start = systemClock.elapsedRealtime() 122 val audioSharingAvailable = audioSharingInteractor.audioSharingAvailable() 123 val deviceItems = 124 bluetoothTileDialogRepository.cachedDevices 125 .mapNotNull { cachedDevice -> 126 deviceItemFactoryList 127 .firstOrNull { 128 it.isFilterMatched( 129 context, 130 cachedDevice, 131 audioManager, 132 audioSharingAvailable, 133 ) 134 } 135 ?.create(context, cachedDevice) 136 } 137 .sort(deviceItemDisplayPriority, bluetoothAdapter?.mostRecentlyConnectedDevices) 138 // Only emit when the job is not cancelled 139 if (isActive) { 140 mutableDeviceItemUpdate.tryEmit(deviceItems.take(MAX_DEVICE_ITEM_ENTRY)) 141 mutableShowSeeAllUpdate.tryEmit(deviceItems.size > MAX_DEVICE_ITEM_ENTRY) 142 logger.logDeviceFetch( 143 JobStatus.FINISHED, 144 trigger, 145 systemClock.elapsedRealtime() - start, 146 ) 147 } else { 148 logger.logDeviceFetch( 149 JobStatus.CANCELLED, 150 trigger, 151 systemClock.elapsedRealtime() - start, 152 ) 153 } 154 } 155 } 156 157 private fun List<DeviceItem>.sort( 158 displayPriority: List<DeviceItemType>, 159 mostRecentlyConnectedDevices: List<BluetoothDevice>?, 160 ): List<DeviceItem> { 161 return this.sortedWith( 162 compareBy<DeviceItem> { displayPriority.indexOf(it.type) } 163 .thenBy { 164 mostRecentlyConnectedDevices?.indexOf(it.cachedBluetoothDevice.device) ?: 0 165 } 166 ) 167 } 168 169 companion object { 170 private const val TAG = "DeviceItemInteractor" 171 private const val MAX_DEVICE_ITEM_ENTRY = 3 172 } 173 } 174