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.BluetoothDevice 20 import android.bluetooth.BluetoothProfile 21 import android.content.Intent 22 import android.os.Bundle 23 import android.provider.Settings 24 import com.android.internal.logging.UiEventLogger 25 import com.android.settingslib.bluetooth.A2dpProfile 26 import com.android.settingslib.bluetooth.BluetoothUtils 27 import com.android.settingslib.bluetooth.HeadsetProfile 28 import com.android.settingslib.bluetooth.HearingAidProfile 29 import com.android.settingslib.bluetooth.LeAudioProfile 30 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast 31 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant 32 import com.android.settingslib.bluetooth.LocalBluetoothManager 33 import com.android.systemui.animation.DialogTransitionAnimator 34 import com.android.systemui.bluetooth.qsdialog.DeviceItemActionInteractor.LaunchSettingsCriteria.Companion.getCurrentConnectedLeByGroupId 35 import com.android.systemui.dagger.SysUISingleton 36 import com.android.systemui.dagger.qualifiers.Background 37 import com.android.systemui.plugins.ActivityStarter 38 import com.android.systemui.statusbar.phone.SystemUIDialog 39 import javax.inject.Inject 40 import kotlinx.coroutines.CoroutineDispatcher 41 import kotlinx.coroutines.withContext 42 43 @SysUISingleton 44 class DeviceItemActionInteractor 45 @Inject 46 constructor( 47 private val activityStarter: ActivityStarter, 48 private val dialogTransitionAnimator: DialogTransitionAnimator, 49 private val localBluetoothManager: LocalBluetoothManager?, 50 @Background private val backgroundDispatcher: CoroutineDispatcher, 51 private val logger: BluetoothTileDialogLogger, 52 private val uiEventLogger: UiEventLogger, 53 ) { 54 private val leAudioProfile: LeAudioProfile? 55 get() = localBluetoothManager?.profileManager?.leAudioProfile 56 57 private val assistantProfile: LocalBluetoothLeBroadcastAssistant? 58 get() = localBluetoothManager?.profileManager?.leAudioBroadcastAssistantProfile 59 60 private val launchSettingsCriteriaList: List<LaunchSettingsCriteria> 61 get() = 62 listOf( 63 InSharingClickedNoSource(localBluetoothManager, backgroundDispatcher, logger), 64 NotSharingClickedNonConnect( 65 leAudioProfile, 66 assistantProfile, 67 backgroundDispatcher, 68 logger 69 ), 70 NotSharingClickedConnected( 71 leAudioProfile, 72 assistantProfile, 73 backgroundDispatcher, 74 logger 75 ) 76 ) 77 78 suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog) { 79 withContext(backgroundDispatcher) { 80 logger.logDeviceClick(deviceItem.cachedBluetoothDevice.address, deviceItem.type) 81 if ( 82 BluetoothUtils.isAudioSharingEnabled() && 83 localBluetoothManager != null && 84 leAudioProfile != null && 85 assistantProfile != null 86 ) { 87 val inAudioSharing = BluetoothUtils.isBroadcasting(localBluetoothManager) 88 logger.logDeviceClickInAudioSharingWhenEnabled(inAudioSharing) 89 90 val criteriaMatched = 91 launchSettingsCriteriaList.firstOrNull { 92 it.matched(inAudioSharing, deviceItem) 93 } 94 if (criteriaMatched != null) { 95 uiEventLogger.log(criteriaMatched.getClickUiEvent(deviceItem)) 96 launchSettings(deviceItem.cachedBluetoothDevice.device, dialog) 97 return@withContext 98 } 99 } 100 deviceItem.cachedBluetoothDevice.apply { 101 when (deviceItem.type) { 102 DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE -> { 103 disconnect() 104 uiEventLogger.log(BluetoothTileDialogUiEvent.ACTIVE_DEVICE_DISCONNECT) 105 } 106 DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> { 107 uiEventLogger.log(BluetoothTileDialogUiEvent.AUDIO_SHARING_DEVICE_CLICKED) 108 } 109 DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE -> { 110 setActive() 111 uiEventLogger.log(BluetoothTileDialogUiEvent.CONNECTED_DEVICE_SET_ACTIVE) 112 } 113 DeviceItemType.CONNECTED_BLUETOOTH_DEVICE -> { 114 disconnect() 115 uiEventLogger.log( 116 BluetoothTileDialogUiEvent.CONNECTED_OTHER_DEVICE_DISCONNECT 117 ) 118 } 119 DeviceItemType.SAVED_BLUETOOTH_DEVICE -> { 120 connect() 121 uiEventLogger.log(BluetoothTileDialogUiEvent.SAVED_DEVICE_CONNECT) 122 } 123 } 124 } 125 } 126 } 127 128 private fun launchSettings(device: BluetoothDevice, dialog: SystemUIDialog) { 129 val intent = 130 Intent(Settings.ACTION_BLUETOOTH_SETTINGS).apply { 131 putExtra( 132 EXTRA_SHOW_FRAGMENT_ARGUMENTS, 133 Bundle().apply { 134 putParcelable(LocalBluetoothLeBroadcast.EXTRA_BLUETOOTH_DEVICE, device) 135 } 136 ) 137 } 138 intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK 139 activityStarter.postStartActivityDismissingKeyguard( 140 intent, 141 0, 142 dialogTransitionAnimator.createActivityTransitionController(dialog) 143 ) 144 } 145 146 private interface LaunchSettingsCriteria { 147 suspend fun matched(inAudioSharing: Boolean, deviceItem: DeviceItem): Boolean 148 149 suspend fun getClickUiEvent(deviceItem: DeviceItem): BluetoothTileDialogUiEvent 150 151 companion object { 152 suspend fun getCurrentConnectedLeByGroupId( 153 leAudioProfile: LeAudioProfile, 154 assistantProfile: LocalBluetoothLeBroadcastAssistant, 155 @Background backgroundDispatcher: CoroutineDispatcher, 156 logger: BluetoothTileDialogLogger, 157 ): Map<Int, List<BluetoothDevice>> { 158 return withContext(backgroundDispatcher) { 159 assistantProfile 160 .getDevicesMatchingConnectionStates( 161 intArrayOf(BluetoothProfile.STATE_CONNECTED) 162 ) 163 ?.filterNotNull() 164 ?.groupBy { leAudioProfile.getGroupId(it) } 165 ?.also { logger.logConnectedLeByGroupId(it) } ?: emptyMap() 166 } 167 } 168 } 169 } 170 171 private class InSharingClickedNoSource( 172 private val localBluetoothManager: LocalBluetoothManager?, 173 @Background private val backgroundDispatcher: CoroutineDispatcher, 174 private val logger: BluetoothTileDialogLogger, 175 ) : LaunchSettingsCriteria { 176 // If currently broadcasting and the clicked device is not connected to the source 177 override suspend fun matched(inAudioSharing: Boolean, deviceItem: DeviceItem): Boolean { 178 return withContext(backgroundDispatcher) { 179 val matched = 180 inAudioSharing && 181 deviceItem.isMediaDevice && 182 !BluetoothUtils.hasConnectedBroadcastSource( 183 deviceItem.cachedBluetoothDevice, 184 localBluetoothManager 185 ) 186 187 if (matched) { 188 logger.logLaunchSettingsCriteriaMatched("InSharingClickedNoSource", deviceItem) 189 } 190 191 matched 192 } 193 } 194 195 override suspend fun getClickUiEvent(deviceItem: DeviceItem) = 196 if (deviceItem.isLeAudioSupported) 197 BluetoothTileDialogUiEvent.LAUNCH_SETTINGS_IN_SHARING_LE_DEVICE_CLICKED 198 else BluetoothTileDialogUiEvent.LAUNCH_SETTINGS_IN_SHARING_NON_LE_DEVICE_CLICKED 199 } 200 201 private class NotSharingClickedNonConnect( 202 private val leAudioProfile: LeAudioProfile?, 203 private val assistantProfile: LocalBluetoothLeBroadcastAssistant?, 204 @Background private val backgroundDispatcher: CoroutineDispatcher, 205 private val logger: BluetoothTileDialogLogger, 206 ) : LaunchSettingsCriteria { 207 // If not broadcasting, having one device connected, and clicked on a not yet connected LE 208 // audio device 209 override suspend fun matched(inAudioSharing: Boolean, deviceItem: DeviceItem): Boolean { 210 return withContext(backgroundDispatcher) { 211 val matched = 212 leAudioProfile?.let { leAudio -> 213 assistantProfile?.let { assistant -> 214 !inAudioSharing && 215 getCurrentConnectedLeByGroupId( 216 leAudio, 217 assistant, 218 backgroundDispatcher, 219 logger 220 ) 221 .size == 1 && 222 deviceItem.isNotConnectedLeAudioSupported 223 } 224 } ?: false 225 226 if (matched) { 227 logger.logLaunchSettingsCriteriaMatched( 228 "NotSharingClickedNonConnect", 229 deviceItem 230 ) 231 } 232 233 matched 234 } 235 } 236 237 override suspend fun getClickUiEvent(deviceItem: DeviceItem) = 238 BluetoothTileDialogUiEvent.LAUNCH_SETTINGS_NOT_SHARING_SAVED_LE_DEVICE_CLICKED 239 } 240 241 private class NotSharingClickedConnected( 242 private val leAudioProfile: LeAudioProfile?, 243 private val assistantProfile: LocalBluetoothLeBroadcastAssistant?, 244 @Background private val backgroundDispatcher: CoroutineDispatcher, 245 private val logger: BluetoothTileDialogLogger, 246 ) : LaunchSettingsCriteria { 247 // If not broadcasting, having two device connected, clicked on any connected LE audio 248 // devices 249 override suspend fun matched(inAudioSharing: Boolean, deviceItem: DeviceItem): Boolean { 250 return withContext(backgroundDispatcher) { 251 val matched = 252 leAudioProfile?.let { leAudio -> 253 assistantProfile?.let { assistant -> 254 !inAudioSharing && 255 getCurrentConnectedLeByGroupId( 256 leAudio, 257 assistant, 258 backgroundDispatcher, 259 logger 260 ) 261 .size == 2 && 262 deviceItem.isActiveOrConnectedLeAudioSupported 263 } 264 } ?: false 265 266 if (matched) { 267 logger.logLaunchSettingsCriteriaMatched( 268 "NotSharingClickedConnected", 269 deviceItem 270 ) 271 } 272 273 matched 274 } 275 } 276 277 override suspend fun getClickUiEvent(deviceItem: DeviceItem) = 278 BluetoothTileDialogUiEvent.LAUNCH_SETTINGS_NOT_SHARING_CONNECTED_LE_DEVICE_CLICKED 279 } 280 281 private companion object { 282 const val EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":settings:show_fragment_args" 283 284 val DeviceItem.isLeAudioSupported: Boolean 285 get() = 286 cachedBluetoothDevice.profiles.any { profile -> 287 profile is LeAudioProfile && profile.isEnabled(cachedBluetoothDevice.device) 288 } 289 290 val DeviceItem.isNotConnectedLeAudioSupported: Boolean 291 get() = type == DeviceItemType.SAVED_BLUETOOTH_DEVICE && isLeAudioSupported 292 293 val DeviceItem.isActiveOrConnectedLeAudioSupported: Boolean 294 get() = 295 (type == DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE || 296 type == DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE) && isLeAudioSupported 297 298 val DeviceItem.isMediaDevice: Boolean 299 get() = 300 cachedBluetoothDevice.connectableProfiles.any { 301 it is A2dpProfile || 302 it is HearingAidProfile || 303 it is LeAudioProfile || 304 it is HeadsetProfile 305 } 306 } 307 } 308