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