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