1 /*
2  * 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.BluetoothDevice
20 import android.content.Context
21 import android.media.AudioManager
22 import com.android.settingslib.bluetooth.BluetoothUtils
23 import com.android.settingslib.bluetooth.CachedBluetoothDevice
24 import com.android.settingslib.bluetooth.LocalBluetoothManager
25 import com.android.settingslib.flags.Flags
26 import com.android.settingslib.flags.Flags.enableLeAudioSharing
27 import com.android.systemui.res.R
28 
29 private val backgroundOn = R.drawable.settingslib_switch_bar_bg_on
30 private val backgroundOff = R.drawable.bluetooth_tile_dialog_bg_off
31 private val backgroundOffBusy = R.drawable.bluetooth_tile_dialog_bg_off_busy
32 private val connected = R.string.quick_settings_bluetooth_device_connected
33 private val audioSharing = R.string.quick_settings_bluetooth_device_audio_sharing
34 private val saved = R.string.quick_settings_bluetooth_device_saved
35 private val actionAccessibilityLabelActivate =
36     R.string.accessibility_quick_settings_bluetooth_device_tap_to_activate
37 private val actionAccessibilityLabelDisconnect =
38     R.string.accessibility_quick_settings_bluetooth_device_tap_to_disconnect
39 
40 /** Factories to create different types of Bluetooth device items from CachedBluetoothDevice. */
41 internal abstract class DeviceItemFactory {
isFilterMatchednull42     abstract fun isFilterMatched(
43         context: Context,
44         cachedDevice: CachedBluetoothDevice,
45         audioManager: AudioManager,
46     ): Boolean
47 
48     abstract fun create(context: Context, cachedDevice: CachedBluetoothDevice): DeviceItem
49 
50     companion object {
51         @JvmStatic
52         fun createDeviceItem(
53             context: Context,
54             cachedDevice: CachedBluetoothDevice,
55             type: DeviceItemType,
56             connectionSummary: String,
57             background: Int,
58             actionAccessibilityLabel: String,
59             isActive: Boolean
60         ): DeviceItem {
61             return DeviceItem(
62                 type = type,
63                 cachedBluetoothDevice = cachedDevice,
64                 deviceName = cachedDevice.name,
65                 connectionSummary = connectionSummary,
66                 iconWithDescription =
67                     BluetoothUtils.getBtClassDrawableWithDescription(context, cachedDevice).let {
68                         Pair(it.first, it.second)
69                     },
70                 background = background,
71                 isEnabled = !cachedDevice.isBusy,
72                 actionAccessibilityLabel = actionAccessibilityLabel,
73                 isActive = isActive
74             )
75         }
76     }
77 }
78 
79 internal open class ActiveMediaDeviceItemFactory : DeviceItemFactory() {
isFilterMatchednull80     override fun isFilterMatched(
81         context: Context,
82         cachedDevice: CachedBluetoothDevice,
83         audioManager: AudioManager
84     ): Boolean {
85         return BluetoothUtils.isActiveMediaDevice(cachedDevice) &&
86             BluetoothUtils.isAvailableMediaBluetoothDevice(cachedDevice, audioManager)
87     }
88 
createnull89     override fun create(context: Context, cachedDevice: CachedBluetoothDevice): DeviceItem {
90         return createDeviceItem(
91             context,
92             cachedDevice,
93             DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE,
94             cachedDevice.connectionSummary ?: "",
95             backgroundOn,
96             context.getString(actionAccessibilityLabelDisconnect),
97             isActive = true
98         )
99     }
100 }
101 
102 internal class AudioSharingMediaDeviceItemFactory(
103     private val localBluetoothManager: LocalBluetoothManager?
104 ) : DeviceItemFactory() {
isFilterMatchednull105     override fun isFilterMatched(
106         context: Context,
107         cachedDevice: CachedBluetoothDevice,
108         audioManager: AudioManager
109     ): Boolean {
110         return enableLeAudioSharing() &&
111             BluetoothUtils.hasConnectedBroadcastSource(cachedDevice, localBluetoothManager)
112     }
113 
createnull114     override fun create(context: Context, cachedDevice: CachedBluetoothDevice): DeviceItem {
115         return createDeviceItem(
116             context,
117             cachedDevice,
118             DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE,
119             cachedDevice.connectionSummary.takeUnless { it.isNullOrEmpty() }
120                 ?: context.getString(audioSharing),
121             if (cachedDevice.isBusy) backgroundOffBusy else backgroundOn,
122             "",
123             isActive = !cachedDevice.isBusy
124         )
125     }
126 }
127 
128 internal class ActiveHearingDeviceItemFactory : ActiveMediaDeviceItemFactory() {
isFilterMatchednull129     override fun isFilterMatched(
130         context: Context,
131         cachedDevice: CachedBluetoothDevice,
132         audioManager: AudioManager
133     ): Boolean {
134         return BluetoothUtils.isActiveMediaDevice(cachedDevice) &&
135             BluetoothUtils.isAvailableHearingDevice(cachedDevice)
136     }
137 }
138 
139 internal open class AvailableMediaDeviceItemFactory : DeviceItemFactory() {
isFilterMatchednull140     override fun isFilterMatched(
141         context: Context,
142         cachedDevice: CachedBluetoothDevice,
143         audioManager: AudioManager
144     ): Boolean {
145         return !BluetoothUtils.isActiveMediaDevice(cachedDevice) &&
146             BluetoothUtils.isAvailableMediaBluetoothDevice(cachedDevice, audioManager)
147     }
148 
createnull149     override fun create(context: Context, cachedDevice: CachedBluetoothDevice): DeviceItem {
150         return createDeviceItem(
151             context,
152             cachedDevice,
153             DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE,
154             cachedDevice.connectionSummary.takeUnless { it.isNullOrEmpty() }
155                 ?: context.getString(connected),
156             if (cachedDevice.isBusy) backgroundOffBusy else backgroundOff,
157             context.getString(actionAccessibilityLabelActivate),
158             isActive = false
159         )
160     }
161 }
162 
163 internal class AvailableHearingDeviceItemFactory : AvailableMediaDeviceItemFactory() {
isFilterMatchednull164     override fun isFilterMatched(
165         context: Context,
166         cachedDevice: CachedBluetoothDevice,
167         audioManager: AudioManager
168     ): Boolean {
169         return !BluetoothUtils.isActiveMediaDevice(cachedDevice) &&
170             BluetoothUtils.isAvailableHearingDevice(cachedDevice)
171     }
172 }
173 
174 internal class ConnectedDeviceItemFactory : DeviceItemFactory() {
isFilterMatchednull175     override fun isFilterMatched(
176         context: Context,
177         cachedDevice: CachedBluetoothDevice,
178         audioManager: AudioManager
179     ): Boolean {
180         return if (Flags.enableHideExclusivelyManagedBluetoothDevice()) {
181             !BluetoothUtils.isExclusivelyManagedBluetoothDevice(context, cachedDevice.device) &&
182                 BluetoothUtils.isConnectedBluetoothDevice(cachedDevice, audioManager)
183         } else {
184             BluetoothUtils.isConnectedBluetoothDevice(cachedDevice, audioManager)
185         }
186     }
187 
createnull188     override fun create(context: Context, cachedDevice: CachedBluetoothDevice): DeviceItem {
189         return createDeviceItem(
190             context,
191             cachedDevice,
192             DeviceItemType.CONNECTED_BLUETOOTH_DEVICE,
193             cachedDevice.connectionSummary.takeUnless { it.isNullOrEmpty() }
194                 ?: context.getString(connected),
195             if (cachedDevice.isBusy) backgroundOffBusy else backgroundOff,
196             context.getString(actionAccessibilityLabelDisconnect),
197             isActive = false
198         )
199     }
200 }
201 
202 internal open class SavedDeviceItemFactory : DeviceItemFactory() {
isFilterMatchednull203     override fun isFilterMatched(
204         context: Context,
205         cachedDevice: CachedBluetoothDevice,
206         audioManager: AudioManager
207     ): Boolean {
208         return if (Flags.enableHideExclusivelyManagedBluetoothDevice()) {
209             !BluetoothUtils.isExclusivelyManagedBluetoothDevice(context, cachedDevice.device) &&
210                 cachedDevice.bondState == BluetoothDevice.BOND_BONDED &&
211                 !cachedDevice.isConnected
212         } else {
213             cachedDevice.bondState == BluetoothDevice.BOND_BONDED && !cachedDevice.isConnected
214         }
215     }
216 
createnull217     override fun create(context: Context, cachedDevice: CachedBluetoothDevice): DeviceItem {
218         return createDeviceItem(
219             context,
220             cachedDevice,
221             DeviceItemType.SAVED_BLUETOOTH_DEVICE,
222             cachedDevice.connectionSummary.takeUnless { it.isNullOrEmpty() }
223                 ?: context.getString(saved),
224             if (cachedDevice.isBusy) backgroundOffBusy else backgroundOff,
225             context.getString(actionAccessibilityLabelActivate),
226             isActive = false
227         )
228     }
229 }
230 
231 internal class SavedHearingDeviceItemFactory : SavedDeviceItemFactory() {
isFilterMatchednull232     override fun isFilterMatched(
233         context: Context,
234         cachedDevice: CachedBluetoothDevice,
235         audioManager: AudioManager
236     ): Boolean {
237         return if (Flags.enableHideExclusivelyManagedBluetoothDevice()) {
238             !BluetoothUtils.isExclusivelyManagedBluetoothDevice(
239                 context,
240                 cachedDevice.getDevice()
241             ) &&
242                 cachedDevice.isHearingAidDevice &&
243                 cachedDevice.bondState == BluetoothDevice.BOND_BONDED &&
244                 !cachedDevice.isConnected
245         } else {
246             cachedDevice.isHearingAidDevice &&
247                 cachedDevice.bondState == BluetoothDevice.BOND_BONDED &&
248                 !cachedDevice.isConnected
249         }
250     }
251 }
252