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