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