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