1 /* <lambda>null2 * Copyright (C) 2020 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.media.controls.domain.pipeline 18 19 import android.bluetooth.BluetoothLeBroadcast 20 import android.bluetooth.BluetoothLeBroadcastMetadata 21 import android.content.Context 22 import android.graphics.drawable.Drawable 23 import android.media.MediaRouter2Manager 24 import android.media.RoutingSessionInfo 25 import android.media.session.MediaController 26 import android.media.session.MediaController.PlaybackInfo 27 import android.text.TextUtils 28 import android.util.Log 29 import androidx.annotation.AnyThread 30 import androidx.annotation.MainThread 31 import androidx.annotation.WorkerThread 32 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast 33 import com.android.settingslib.bluetooth.LocalBluetoothManager 34 import com.android.settingslib.flags.Flags.enableLeAudioSharing 35 import com.android.settingslib.flags.Flags.legacyLeAudioSharing 36 import com.android.settingslib.media.LocalMediaManager 37 import com.android.settingslib.media.MediaDevice 38 import com.android.settingslib.media.PhoneMediaDevice 39 import com.android.settingslib.media.flags.Flags 40 import com.android.systemui.Flags.mediaControlsDeviceManagerBackgroundExecution 41 import com.android.systemui.dagger.qualifiers.Background 42 import com.android.systemui.dagger.qualifiers.Main 43 import com.android.systemui.media.controls.shared.MediaControlDrawables 44 import com.android.systemui.media.controls.shared.model.MediaData 45 import com.android.systemui.media.controls.shared.model.MediaDeviceData 46 import com.android.systemui.media.controls.util.LocalMediaManagerFactory 47 import com.android.systemui.media.controls.util.MediaControllerFactory 48 import com.android.systemui.media.controls.util.MediaDataUtils 49 import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManager 50 import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManagerFactory 51 import com.android.systemui.res.R 52 import com.android.systemui.statusbar.policy.ConfigurationController 53 import dagger.Lazy 54 import java.io.PrintWriter 55 import java.util.concurrent.Executor 56 import javax.inject.Inject 57 58 private const val PLAYBACK_TYPE_UNKNOWN = 0 59 private const val TAG = "MediaDeviceManager" 60 private const val DEBUG = true 61 62 /** Provides information about the route (ie. device) where playback is occurring. */ 63 class MediaDeviceManager 64 @Inject 65 constructor( 66 private val context: Context, 67 private val controllerFactory: MediaControllerFactory, 68 private val localMediaManagerFactory: LocalMediaManagerFactory, 69 private val mr2manager: Lazy<MediaRouter2Manager>, 70 private val muteAwaitConnectionManagerFactory: MediaMuteAwaitConnectionManagerFactory, 71 private val configurationController: ConfigurationController, 72 private val localBluetoothManager: Lazy<LocalBluetoothManager?>, 73 @Main private val fgExecutor: Executor, 74 @Background private val bgExecutor: Executor, 75 private val logger: MediaDeviceLogger, 76 ) : MediaDataManager.Listener { 77 78 private val listeners: MutableSet<Listener> = mutableSetOf() 79 private val entries: MutableMap<String, Entry> = mutableMapOf() 80 81 companion object { 82 private val EMPTY_AND_DISABLED_MEDIA_DEVICE_DATA = 83 MediaDeviceData(enabled = false, icon = null, name = null, showBroadcastButton = false) 84 } 85 86 /** Add a listener for changes to the media route (ie. device). */ 87 fun addListener(listener: Listener) = listeners.add(listener) 88 89 /** Remove a listener that has been registered with addListener. */ 90 fun removeListener(listener: Listener) = listeners.remove(listener) 91 92 override fun onMediaDataLoaded( 93 key: String, 94 oldKey: String?, 95 data: MediaData, 96 immediately: Boolean, 97 receivedSmartspaceCardLatency: Int, 98 isSsReactivated: Boolean, 99 ) { 100 if (mediaControlsDeviceManagerBackgroundExecution()) { 101 bgExecutor.execute { onMediaLoaded(key, oldKey, data) } 102 } else { 103 onMediaLoaded(key, oldKey, data) 104 } 105 } 106 107 override fun onMediaDataRemoved(key: String, userInitiated: Boolean) { 108 if (mediaControlsDeviceManagerBackgroundExecution()) { 109 bgExecutor.execute { onMediaRemoved(key, userInitiated) } 110 } else { 111 onMediaRemoved(key, userInitiated) 112 } 113 } 114 115 fun dump(pw: PrintWriter) { 116 with(pw) { 117 println("MediaDeviceManager state:") 118 entries.forEach { (key, entry) -> 119 println(" key=$key") 120 entry.dump(pw) 121 } 122 } 123 } 124 125 @MainThread 126 private fun processDevice(key: String, oldKey: String?, device: MediaDeviceData?) { 127 listeners.forEach { it.onMediaDeviceChanged(key, oldKey, device) } 128 } 129 130 private fun onMediaLoaded(key: String, oldKey: String?, data: MediaData) { 131 if (oldKey != null && oldKey != key) { 132 val oldEntry = entries.remove(oldKey) 133 oldEntry?.stop() 134 } 135 var entry = entries[key] 136 if (entry == null || entry.token != data.token) { 137 entry?.stop() 138 if (data.device != null) { 139 // If we were already provided device info (e.g. from RCN), keep that and 140 // don't listen for updates, but process once to push updates to listeners 141 if (mediaControlsDeviceManagerBackgroundExecution()) { 142 fgExecutor.execute { processDevice(key, oldKey, data.device) } 143 } else { 144 processDevice(key, oldKey, data.device) 145 } 146 return 147 } 148 val controller = data.token?.let { controllerFactory.create(it) } 149 val localMediaManager = 150 localMediaManagerFactory.create(data.packageName, controller?.sessionToken) 151 val muteAwaitConnectionManager = 152 muteAwaitConnectionManagerFactory.create(localMediaManager) 153 entry = Entry(key, oldKey, controller, localMediaManager, muteAwaitConnectionManager) 154 entries[key] = entry 155 entry.start() 156 } 157 } 158 159 private fun onMediaRemoved(key: String, userInitiated: Boolean) { 160 val token = entries.remove(key) 161 token?.stop() 162 if (mediaControlsDeviceManagerBackgroundExecution()) { 163 fgExecutor.execute { 164 token?.let { listeners.forEach { it.onKeyRemoved(key, userInitiated) } } 165 } 166 } else { 167 token?.let { listeners.forEach { it.onKeyRemoved(key, userInitiated) } } 168 } 169 } 170 171 interface Listener { 172 /** Called when the route has changed for a given notification. */ 173 fun onMediaDeviceChanged(key: String, oldKey: String?, data: MediaDeviceData?) 174 175 /** Called when the notification was removed. */ 176 fun onKeyRemoved(key: String, userInitiated: Boolean) 177 } 178 179 private inner class Entry( 180 val key: String, 181 val oldKey: String?, 182 val controller: MediaController?, 183 val localMediaManager: LocalMediaManager, 184 val muteAwaitConnectionManager: MediaMuteAwaitConnectionManager, 185 ) : 186 LocalMediaManager.DeviceCallback, 187 MediaController.Callback(), 188 BluetoothLeBroadcast.Callback { 189 190 val token 191 get() = controller?.sessionToken 192 193 private var started = false 194 private var playbackType = PLAYBACK_TYPE_UNKNOWN 195 private var playbackVolumeControlId: String? = null 196 private var current: MediaDeviceData? = null 197 set(value) { 198 val sameWithoutIcon = value != null && value.equalsWithoutIcon(field) 199 if (!started || !sameWithoutIcon) { 200 field = value 201 fgExecutor.execute { processDevice(key, oldKey, value) } 202 } 203 } 204 205 // A device that is not yet connected but is expected to connect imminently. Because it's 206 // expected to connect imminently, it should be displayed as the current device. 207 private var aboutToConnectDeviceOverride: AboutToConnectDevice? = null 208 private var broadcastDescription: String? = null 209 private val configListener = 210 object : ConfigurationController.ConfigurationListener { 211 override fun onLocaleListChanged() { 212 updateCurrent() 213 } 214 } 215 216 @AnyThread 217 fun start() = 218 bgExecutor.execute { 219 if (!started) { 220 localMediaManager.registerCallback(this) 221 if (!Flags.removeUnnecessaryRouteScanning()) { 222 localMediaManager.startScan() 223 } 224 muteAwaitConnectionManager.startListening() 225 playbackType = controller?.playbackInfo?.playbackType ?: PLAYBACK_TYPE_UNKNOWN 226 playbackVolumeControlId = controller?.playbackInfo?.volumeControlId 227 controller?.registerCallback(this) 228 updateCurrent() 229 started = true 230 configurationController.addCallback(configListener) 231 } 232 } 233 234 @AnyThread 235 fun stop() = 236 bgExecutor.execute { 237 if (started) { 238 started = false 239 controller?.unregisterCallback(this) 240 if (!Flags.removeUnnecessaryRouteScanning()) { 241 localMediaManager.stopScan() 242 } 243 localMediaManager.unregisterCallback(this) 244 muteAwaitConnectionManager.stopListening() 245 configurationController.removeCallback(configListener) 246 } 247 } 248 249 fun dump(pw: PrintWriter) { 250 val routingSession = 251 controller?.let { mr2manager.get().getRoutingSessionForMediaController(it) } 252 val selectedRoutes = routingSession?.let { mr2manager.get().getSelectedRoutes(it) } 253 with(pw) { 254 println(" current device is ${current?.name}") 255 val type = controller?.playbackInfo?.playbackType 256 println(" PlaybackType=$type (1 for local, 2 for remote) cached=$playbackType") 257 val volumeControlId = controller?.playbackInfo?.volumeControlId 258 println(" volumeControlId=$volumeControlId cached= $playbackVolumeControlId") 259 println(" routingSession=$routingSession") 260 println(" selectedRoutes=$selectedRoutes") 261 println(" currentConnectedDevice=${localMediaManager.currentConnectedDevice}") 262 } 263 } 264 265 @WorkerThread 266 override fun onAudioInfoChanged(info: MediaController.PlaybackInfo) { 267 val newPlaybackType = info.playbackType 268 val newPlaybackVolumeControlId = info.volumeControlId 269 if ( 270 newPlaybackType == playbackType && 271 newPlaybackVolumeControlId == playbackVolumeControlId 272 ) { 273 return 274 } 275 playbackType = newPlaybackType 276 playbackVolumeControlId = newPlaybackVolumeControlId 277 updateCurrent() 278 } 279 280 override fun onDeviceListUpdate(devices: List<MediaDevice>?) = 281 bgExecutor.execute { updateCurrent() } 282 283 override fun onSelectedDeviceStateChanged(device: MediaDevice, state: Int) { 284 bgExecutor.execute { updateCurrent() } 285 } 286 287 override fun onAboutToConnectDeviceAdded( 288 deviceAddress: String, 289 deviceName: String, 290 deviceIcon: Drawable?, 291 ) { 292 aboutToConnectDeviceOverride = 293 AboutToConnectDevice( 294 fullMediaDevice = localMediaManager.getMediaDeviceById(deviceAddress), 295 backupMediaDeviceData = 296 MediaDeviceData( 297 /* enabled */ enabled = true, 298 /* icon */ deviceIcon, 299 /* name */ deviceName, 300 /* showBroadcastButton */ showBroadcastButton = false, 301 ), 302 ) 303 updateCurrent() 304 } 305 306 override fun onAboutToConnectDeviceRemoved() { 307 aboutToConnectDeviceOverride = null 308 updateCurrent() 309 } 310 311 override fun onBroadcastStarted(reason: Int, broadcastId: Int) { 312 logger.logBroadcastEvent("onBroadcastStarted", reason, broadcastId) 313 updateCurrent() 314 } 315 316 override fun onBroadcastStartFailed(reason: Int) { 317 logger.logBroadcastEvent("onBroadcastStartFailed", reason) 318 } 319 320 override fun onBroadcastMetadataChanged( 321 broadcastId: Int, 322 metadata: BluetoothLeBroadcastMetadata, 323 ) { 324 logger.logBroadcastMetadataChanged(broadcastId, metadata.toString()) 325 updateCurrent() 326 } 327 328 override fun onBroadcastStopped(reason: Int, broadcastId: Int) { 329 logger.logBroadcastEvent("onBroadcastStopped", reason, broadcastId) 330 updateCurrent() 331 } 332 333 override fun onBroadcastStopFailed(reason: Int) { 334 logger.logBroadcastEvent("onBroadcastStopFailed", reason) 335 } 336 337 override fun onBroadcastUpdated(reason: Int, broadcastId: Int) { 338 logger.logBroadcastEvent("onBroadcastUpdated", reason, broadcastId) 339 updateCurrent() 340 } 341 342 override fun onBroadcastUpdateFailed(reason: Int, broadcastId: Int) { 343 logger.logBroadcastEvent("onBroadcastUpdateFailed", reason, broadcastId) 344 } 345 346 override fun onPlaybackStarted(reason: Int, broadcastId: Int) {} 347 348 override fun onPlaybackStopped(reason: Int, broadcastId: Int) {} 349 350 @WorkerThread 351 private fun updateCurrent() { 352 if (isLeAudioBroadcastEnabled()) { 353 current = getLeAudioBroadcastDeviceData() 354 } else if (Flags.usePlaybackInfoForRoutingControls()) { 355 val activeDevice: MediaDeviceData? 356 357 // LocalMediaManager provides the connected device based on PlaybackInfo. 358 // TODO (b/342197065): Simplify nullability once we make currentConnectedDevice 359 // non-null. 360 val connectedDevice = localMediaManager.currentConnectedDevice?.toMediaDeviceData() 361 362 if (controller?.playbackInfo?.playbackType == PlaybackInfo.PLAYBACK_TYPE_REMOTE) { 363 val routingSession = 364 mr2manager.get().getRoutingSessionForMediaController(controller) 365 366 activeDevice = 367 routingSession?.let { 368 val icon = 369 if (it.selectedRoutes.size > 1) { 370 MediaControlDrawables.getGroupDevice(context) 371 } else { 372 connectedDevice?.icon // Single route. We don't change the icon. 373 } 374 // For a remote session, always use the current device from 375 // LocalMediaManager. Override with routing session information if 376 // available: 377 // - Name: To show the dynamic group name. 378 // - Icon: To show the group icon if there's more than one selected 379 // route. 380 connectedDevice?.copy( 381 name = it.name ?: connectedDevice.name, 382 icon = icon, 383 ) 384 } 385 ?: MediaDeviceData( 386 enabled = false, 387 icon = MediaControlDrawables.getHomeDevices(context), 388 name = context.getString(R.string.media_seamless_other_device), 389 showBroadcastButton = false, 390 ) 391 logger.logRemoteDevice(routingSession?.name, connectedDevice) 392 } else { 393 // Prefer SASS if available when playback is local. 394 val sassDevice = getSassDevice() 395 activeDevice = sassDevice ?: connectedDevice 396 logger.logLocalDevice(sassDevice, connectedDevice) 397 } 398 399 current = activeDevice ?: EMPTY_AND_DISABLED_MEDIA_DEVICE_DATA 400 logger.logNewDeviceName(current?.name?.toString()) 401 } else { 402 val aboutToConnect = aboutToConnectDeviceOverride 403 if ( 404 aboutToConnect != null && 405 aboutToConnect.fullMediaDevice == null && 406 aboutToConnect.backupMediaDeviceData != null 407 ) { 408 // Only use [backupMediaDeviceData] when we don't have [fullMediaDevice]. 409 current = aboutToConnect.backupMediaDeviceData 410 return 411 } 412 val device = 413 aboutToConnect?.fullMediaDevice ?: localMediaManager.currentConnectedDevice 414 val routingSession = 415 controller?.let { mr2manager.get().getRoutingSessionForMediaController(it) } 416 417 // If we have a controller but get a null route, then don't trust the device 418 val enabled = device != null && (controller == null || routingSession != null) 419 420 val name = getDeviceName(device, routingSession) 421 logger.logNewDeviceName(name) 422 current = 423 MediaDeviceData( 424 enabled, 425 device?.iconWithoutBackground, 426 name, 427 id = device?.id, 428 showBroadcastButton = false, 429 ) 430 } 431 } 432 433 private fun getSassDevice(): MediaDeviceData? { 434 val sassDevice = aboutToConnectDeviceOverride ?: return null 435 return sassDevice.fullMediaDevice?.toMediaDeviceData() 436 ?: sassDevice.backupMediaDeviceData 437 } 438 439 private fun MediaDevice.toMediaDeviceData() = 440 MediaDeviceData( 441 enabled = true, 442 icon = iconWithoutBackground, 443 name = name, 444 id = id, 445 showBroadcastButton = false, 446 ) 447 448 private fun getLeAudioBroadcastDeviceData(): MediaDeviceData { 449 return if (enableLeAudioSharing()) { 450 MediaDeviceData( 451 enabled = false, 452 icon = MediaControlDrawables.getLeAudioSharing(context), 453 name = context.getString(R.string.audio_sharing_description), 454 intent = null, 455 showBroadcastButton = false, 456 ) 457 } else { 458 MediaDeviceData( 459 enabled = true, 460 icon = MediaControlDrawables.getAntenna(context), 461 name = broadcastDescription, 462 intent = null, 463 showBroadcastButton = true, 464 ) 465 } 466 } 467 468 /** Return a display name for the current device / route, or null if not possible */ 469 private fun getDeviceName( 470 device: MediaDevice?, 471 routingSession: RoutingSessionInfo?, 472 ): String? { 473 val selectedRoutes = routingSession?.let { mr2manager.get().getSelectedRoutes(it) } 474 475 logger.logDeviceName( 476 device, 477 controller, 478 routingSession?.name, 479 selectedRoutes?.firstOrNull()?.name, 480 ) 481 482 if (controller == null) { 483 // In resume state, we don't have a controller - just use the device name 484 return device?.name 485 } 486 487 if (routingSession == null) { 488 // This happens when casting from apps that do not support MediaRouter2 489 // The output switcher can't show anything useful here, so set to null 490 return null 491 } 492 493 // If this is a user route (app / cast provided), use the provided name 494 if (!routingSession.isSystemSession) { 495 return routingSession.name?.toString() ?: device?.name 496 } 497 498 selectedRoutes?.firstOrNull()?.let { 499 if (device is PhoneMediaDevice) { 500 // Get the (localized) name for this phone device 501 return PhoneMediaDevice.getSystemRouteNameFromType(context, it) 502 } else { 503 // If it's another type of device (in practice, Bluetooth), use the route name 504 return it.name.toString() 505 } 506 } 507 return null 508 } 509 510 @WorkerThread 511 private fun isLeAudioBroadcastEnabled(): Boolean { 512 if (!enableLeAudioSharing() && !legacyLeAudioSharing()) return false 513 val localBluetoothManager = localBluetoothManager.get() 514 if (localBluetoothManager != null) { 515 val profileManager = localBluetoothManager.profileManager 516 if (profileManager != null) { 517 val bluetoothLeBroadcast = profileManager.leAudioBroadcastProfile 518 if (bluetoothLeBroadcast != null && bluetoothLeBroadcast.isEnabled(null)) { 519 getBroadcastingInfo(bluetoothLeBroadcast) 520 return true 521 } else if (DEBUG) { 522 Log.d(TAG, "Can not get LocalBluetoothLeBroadcast") 523 } 524 } else if (DEBUG) { 525 Log.d(TAG, "Can not get LocalBluetoothProfileManager") 526 } 527 } else if (DEBUG) { 528 Log.d(TAG, "Can not get LocalBluetoothManager") 529 } 530 return false 531 } 532 533 @WorkerThread 534 private fun getBroadcastingInfo(bluetoothLeBroadcast: LocalBluetoothLeBroadcast) { 535 val currentBroadcastedApp = bluetoothLeBroadcast.appSourceName 536 // TODO(b/233698402): Use the package name instead of app label to avoid the 537 // unexpected result. 538 // Check the current media app's name is the same with current broadcast app's name 539 // or not. 540 val mediaApp = 541 MediaDataUtils.getAppLabel( 542 context, 543 localMediaManager.packageName, 544 context.getString(R.string.bt_le_audio_broadcast_dialog_unknown_name), 545 ) 546 val isCurrentBroadcastedApp = TextUtils.equals(mediaApp, currentBroadcastedApp) 547 if (isCurrentBroadcastedApp) { 548 broadcastDescription = 549 context.getString(R.string.broadcasting_description_is_broadcasting) 550 } else { 551 broadcastDescription = currentBroadcastedApp 552 } 553 } 554 } 555 } 556 557 /** 558 * A class storing information for the about-to-connect device. See 559 * [LocalMediaManager.DeviceCallback.onAboutToConnectDeviceAdded] for more information. 560 * 561 * @property fullMediaDevice a full-fledged [MediaDevice] object representing the device. If 562 * non-null, prefer using [fullMediaDevice] over [backupMediaDeviceData]. 563 * @property backupMediaDeviceData a backup [MediaDeviceData] object containing the minimum 564 * information required to display the device. Only use if [fullMediaDevice] is null. 565 */ 566 private data class AboutToConnectDevice( 567 val fullMediaDevice: MediaDevice? = null, 568 val backupMediaDeviceData: MediaDeviceData? = null, 569 ) 570