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 com.android.settingslib.bluetooth.BluetoothUtils 22 import com.android.settingslib.bluetooth.CachedBluetoothDevice 23 import com.android.settingslib.bluetooth.LocalBluetoothManager 24 import com.android.systemui.res.R 25 26 private val backgroundOn = R.drawable.settingslib_switch_bar_bg_on 27 private val backgroundOff = R.drawable.bluetooth_tile_dialog_bg_off 28 private val backgroundOffBusy = R.drawable.bluetooth_tile_dialog_bg_off_busy 29 private val connected = R.string.quick_settings_bluetooth_device_connected 30 private val audioSharing = R.string.quick_settings_bluetooth_device_audio_sharing 31 private val audioSharingAddIcon = R.drawable.ic_add 32 private val audioSharingOnGoingIcon = R.drawable.ic_check 33 private val saved = R.string.quick_settings_bluetooth_device_saved 34 private val actionAccessibilityLabelActivate = 35 R.string.accessibility_quick_settings_bluetooth_device_tap_to_activate 36 private val actionAccessibilityLabelDisconnect = 37 R.string.accessibility_quick_settings_bluetooth_device_tap_to_disconnect 38 39 /** Factories to create different types of Bluetooth device items from CachedBluetoothDevice. */ 40 abstract class DeviceItemFactory { isFilterMatchednull41 abstract fun isFilterMatched( 42 context: Context, 43 cachedDevice: CachedBluetoothDevice, 44 isOngoingCall: Boolean, 45 audioSharingAvailable: Boolean, 46 ): Boolean 47 48 // Overloaded that defaults audioSharingAvailable to false 49 fun isFilterMatched( 50 context: Context, 51 cachedDevice: CachedBluetoothDevice, 52 isOngoingCall: Boolean, 53 ): Boolean = isFilterMatched(context, cachedDevice, isOngoingCall, false) 54 55 abstract fun create(context: Context, cachedDevice: CachedBluetoothDevice): DeviceItem 56 57 companion object { 58 @JvmStatic 59 fun createDeviceItem( 60 cachedDevice: CachedBluetoothDevice, 61 type: DeviceItemType, 62 connectionSummary: String, 63 background: Int, 64 actionAccessibilityLabel: String, 65 isActive: Boolean, 66 actionIconRes: Int = R.drawable.ic_settings_24dp, 67 ): DeviceItem { 68 return DeviceItem( 69 type = type, 70 cachedBluetoothDevice = cachedDevice, 71 deviceName = cachedDevice.name, 72 connectionSummary = connectionSummary, 73 iconWithDescription = 74 cachedDevice.drawableWithDescription.let { Pair(it.first, it.second) }, 75 background = background, 76 isEnabled = !cachedDevice.isBusy, 77 actionAccessibilityLabel = actionAccessibilityLabel, 78 isActive = isActive, 79 actionIconRes = actionIconRes, 80 ) 81 } 82 } 83 } 84 85 internal open class ActiveMediaDeviceItemFactory : DeviceItemFactory() { isFilterMatchednull86 override fun isFilterMatched( 87 context: Context, 88 cachedDevice: CachedBluetoothDevice, 89 isOngoingCall: Boolean, 90 audioSharingAvailable: Boolean, 91 ): Boolean { 92 return BluetoothUtils.isActiveMediaDevice(cachedDevice) && 93 BluetoothUtils.isAvailableMediaBluetoothDevice(cachedDevice, isOngoingCall) 94 } 95 createnull96 override fun create(context: Context, cachedDevice: CachedBluetoothDevice): DeviceItem { 97 return createDeviceItem( 98 cachedDevice, 99 DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE, 100 cachedDevice.connectionSummary ?: "", 101 backgroundOn, 102 context.getString(actionAccessibilityLabelDisconnect), 103 isActive = true, 104 ) 105 } 106 } 107 108 internal class AudioSharingMediaDeviceItemFactory( 109 private val localBluetoothManager: LocalBluetoothManager 110 ) : DeviceItemFactory() { isFilterMatchednull111 override fun isFilterMatched( 112 context: Context, 113 cachedDevice: CachedBluetoothDevice, 114 isOngoingCall: Boolean, 115 audioSharingAvailable: Boolean, 116 ): Boolean { 117 return audioSharingAvailable && 118 !isOngoingCall && 119 BluetoothUtils.hasConnectedBroadcastSource(cachedDevice, localBluetoothManager) 120 } 121 createnull122 override fun create(context: Context, cachedDevice: CachedBluetoothDevice): DeviceItem { 123 return createDeviceItem( 124 cachedDevice, 125 DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE, 126 cachedDevice.connectionSummary.takeUnless { it.isNullOrEmpty() } 127 ?: context.getString(audioSharing), 128 if (cachedDevice.isBusy) backgroundOffBusy else backgroundOn, 129 "", 130 isActive = !cachedDevice.isBusy, 131 actionIconRes = audioSharingOnGoingIcon, 132 ) 133 } 134 } 135 136 internal class AvailableAudioSharingMediaDeviceItemFactory( 137 private val localBluetoothManager: LocalBluetoothManager 138 ) : AvailableMediaDeviceItemFactory() { isFilterMatchednull139 override fun isFilterMatched( 140 context: Context, 141 cachedDevice: CachedBluetoothDevice, 142 isOngoingCall: Boolean, 143 audioSharingAvailable: Boolean, 144 ): Boolean { 145 return audioSharingAvailable && 146 !isOngoingCall && 147 super.isFilterMatched(context, cachedDevice, false, true) && 148 BluetoothUtils.isAvailableAudioSharingMediaBluetoothDevice( 149 cachedDevice, 150 localBluetoothManager, 151 ) 152 } 153 createnull154 override fun create(context: Context, cachedDevice: CachedBluetoothDevice): DeviceItem { 155 return createDeviceItem( 156 cachedDevice, 157 DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE, 158 context.getString( 159 R.string.quick_settings_bluetooth_device_audio_sharing_or_switch_active 160 ), 161 if (cachedDevice.isBusy) backgroundOffBusy else backgroundOff, 162 "", 163 isActive = false, 164 actionIconRes = audioSharingAddIcon, 165 ) 166 } 167 } 168 169 internal class ActiveHearingDeviceItemFactory : ActiveMediaDeviceItemFactory() { isFilterMatchednull170 override fun isFilterMatched( 171 context: Context, 172 cachedDevice: CachedBluetoothDevice, 173 isOngoingCall: Boolean, 174 audioSharingAvailable: Boolean, 175 ): Boolean { 176 return BluetoothUtils.isActiveMediaDevice(cachedDevice) && 177 BluetoothUtils.isAvailableHearingDevice(cachedDevice) 178 } 179 } 180 181 open class AvailableMediaDeviceItemFactory : DeviceItemFactory() { isFilterMatchednull182 override fun isFilterMatched( 183 context: Context, 184 cachedDevice: CachedBluetoothDevice, 185 isOngoingCall: Boolean, 186 audioSharingAvailable: Boolean, 187 ): Boolean { 188 return !BluetoothUtils.isActiveMediaDevice(cachedDevice) && 189 BluetoothUtils.isAvailableMediaBluetoothDevice(cachedDevice, isOngoingCall) 190 } 191 createnull192 override fun create(context: Context, cachedDevice: CachedBluetoothDevice): DeviceItem { 193 return createDeviceItem( 194 cachedDevice, 195 DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE, 196 cachedDevice.connectionSummary.takeUnless { it.isNullOrEmpty() } 197 ?: context.getString(connected), 198 if (cachedDevice.isBusy) backgroundOffBusy else backgroundOff, 199 context.getString(actionAccessibilityLabelActivate), 200 isActive = false, 201 ) 202 } 203 } 204 205 internal class AvailableHearingDeviceItemFactory : AvailableMediaDeviceItemFactory() { isFilterMatchednull206 override fun isFilterMatched( 207 context: Context, 208 cachedDevice: CachedBluetoothDevice, 209 isOngoingCall: Boolean, 210 audioSharingAvailable: Boolean, 211 ): Boolean { 212 return !BluetoothUtils.isActiveMediaDevice(cachedDevice) && 213 BluetoothUtils.isAvailableHearingDevice(cachedDevice) 214 } 215 } 216 217 internal open class ConnectedDeviceItemFactory : DeviceItemFactory() { isFilterMatchednull218 override fun isFilterMatched( 219 context: Context, 220 cachedDevice: CachedBluetoothDevice, 221 isOngoingCall: Boolean, 222 audioSharingAvailable: Boolean, 223 ): Boolean { 224 return !BluetoothUtils.isExclusivelyManagedBluetoothDevice(context, cachedDevice.device) && 225 BluetoothUtils.isConnectedBluetoothDevice(cachedDevice, isOngoingCall) 226 } 227 createnull228 override fun create(context: Context, cachedDevice: CachedBluetoothDevice): DeviceItem { 229 return createDeviceItem( 230 cachedDevice, 231 DeviceItemType.CONNECTED_BLUETOOTH_DEVICE, 232 cachedDevice.connectionSummary.takeUnless { it.isNullOrEmpty() } 233 ?: context.getString(connected), 234 if (cachedDevice.isBusy) backgroundOffBusy else backgroundOff, 235 context.getString(actionAccessibilityLabelDisconnect), 236 isActive = false, 237 ) 238 } 239 } 240 241 internal class ConnectedHearingDeviceItemFactory : ConnectedDeviceItemFactory() { isFilterMatchednull242 override fun isFilterMatched( 243 context: Context, 244 cachedDevice: CachedBluetoothDevice, 245 isOngoingCall: Boolean, 246 audioSharingAvailable: Boolean, 247 ): Boolean { 248 return cachedDevice.isHearingDevice && 249 cachedDevice.bondState == BluetoothDevice.BOND_BONDED && 250 cachedDevice.device.isConnected 251 } 252 } 253 254 internal open class SavedDeviceItemFactory : DeviceItemFactory() { isFilterMatchednull255 override fun isFilterMatched( 256 context: Context, 257 cachedDevice: CachedBluetoothDevice, 258 isOngoingCall: Boolean, 259 audioSharingAvailable: Boolean, 260 ): Boolean { 261 return !BluetoothUtils.isExclusivelyManagedBluetoothDevice(context, cachedDevice.device) && 262 cachedDevice.bondState == BluetoothDevice.BOND_BONDED && 263 !cachedDevice.isConnected 264 } 265 createnull266 override fun create(context: Context, cachedDevice: CachedBluetoothDevice): DeviceItem { 267 return createDeviceItem( 268 cachedDevice, 269 DeviceItemType.SAVED_BLUETOOTH_DEVICE, 270 cachedDevice.connectionSummary.takeUnless { it.isNullOrEmpty() } 271 ?: context.getString(saved), 272 if (cachedDevice.isBusy) backgroundOffBusy else backgroundOff, 273 context.getString(actionAccessibilityLabelActivate), 274 isActive = false, 275 ) 276 } 277 } 278 279 internal class SavedHearingDeviceItemFactory : SavedDeviceItemFactory() { isFilterMatchednull280 override fun isFilterMatched( 281 context: Context, 282 cachedDevice: CachedBluetoothDevice, 283 isOngoingCall: Boolean, 284 audioSharingAvailable: Boolean, 285 ): Boolean { 286 return !BluetoothUtils.isExclusivelyManagedBluetoothDevice( 287 context, 288 cachedDevice.getDevice(), 289 ) && 290 cachedDevice.isHearingDevice && 291 cachedDevice.bondState == BluetoothDevice.BOND_BONDED && 292 !cachedDevice.isConnected 293 } 294 } 295