1 /* 2 * 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.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.session.MediaController 25 import android.text.TextUtils 26 import android.util.Log 27 import androidx.annotation.AnyThread 28 import androidx.annotation.MainThread 29 import androidx.annotation.WorkerThread 30 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast 31 import com.android.settingslib.bluetooth.LocalBluetoothManager 32 import com.android.settingslib.media.LocalMediaManager 33 import com.android.settingslib.media.MediaDevice 34 import com.android.systemui.Dumpable 35 import com.android.systemui.R 36 import com.android.systemui.dagger.qualifiers.Background 37 import com.android.systemui.dagger.qualifiers.Main 38 import com.android.systemui.dump.DumpManager 39 import com.android.systemui.media.controls.models.player.MediaData 40 import com.android.systemui.media.controls.models.player.MediaDeviceData 41 import com.android.systemui.media.controls.util.MediaControllerFactory 42 import com.android.systemui.media.controls.util.MediaDataUtils 43 import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManager 44 import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManagerFactory 45 import com.android.systemui.statusbar.policy.ConfigurationController 46 import java.io.PrintWriter 47 import java.util.concurrent.Executor 48 import javax.inject.Inject 49 50 private const val PLAYBACK_TYPE_UNKNOWN = 0 51 private const val TAG = "MediaDeviceManager" 52 private const val DEBUG = true 53 54 /** Provides information about the route (ie. device) where playback is occurring. */ 55 class MediaDeviceManager 56 @Inject 57 constructor( 58 private val context: Context, 59 private val controllerFactory: MediaControllerFactory, 60 private val localMediaManagerFactory: LocalMediaManagerFactory, 61 private val mr2manager: MediaRouter2Manager, 62 private val muteAwaitConnectionManagerFactory: MediaMuteAwaitConnectionManagerFactory, 63 private val configurationController: ConfigurationController, 64 private val localBluetoothManager: LocalBluetoothManager?, 65 @Main private val fgExecutor: Executor, 66 @Background private val bgExecutor: Executor, 67 dumpManager: DumpManager 68 ) : MediaDataManager.Listener, Dumpable { 69 70 private val listeners: MutableSet<Listener> = mutableSetOf() 71 private val entries: MutableMap<String, Entry> = mutableMapOf() 72 73 init { 74 dumpManager.registerDumpable(javaClass.name, this) 75 } 76 77 /** Add a listener for changes to the media route (ie. device). */ 78 fun addListener(listener: Listener) = listeners.add(listener) 79 80 /** Remove a listener that has been registered with addListener. */ 81 fun removeListener(listener: Listener) = listeners.remove(listener) 82 83 override fun onMediaDataLoaded( 84 key: String, 85 oldKey: String?, 86 data: MediaData, 87 immediately: Boolean, 88 receivedSmartspaceCardLatency: Int, 89 isSsReactivated: Boolean 90 ) { 91 if (oldKey != null && oldKey != key) { 92 val oldEntry = entries.remove(oldKey) 93 oldEntry?.stop() 94 } 95 var entry = entries[key] 96 if (entry == null || entry.token != data.token) { 97 entry?.stop() 98 if (data.device != null) { 99 // If we were already provided device info (e.g. from RCN), keep that and don't 100 // listen for updates, but process once to push updates to listeners 101 processDevice(key, oldKey, data.device) 102 return 103 } 104 val controller = data.token?.let { controllerFactory.create(it) } 105 val localMediaManager = localMediaManagerFactory.create(data.packageName) 106 val muteAwaitConnectionManager = 107 muteAwaitConnectionManagerFactory.create(localMediaManager) 108 entry = Entry(key, oldKey, controller, localMediaManager, muteAwaitConnectionManager) 109 entries[key] = entry 110 entry.start() 111 } 112 } 113 114 override fun onMediaDataRemoved(key: String) { 115 val token = entries.remove(key) 116 token?.stop() 117 token?.let { listeners.forEach { it.onKeyRemoved(key) } } 118 } 119 120 override fun dump(pw: PrintWriter, args: Array<String>) { 121 with(pw) { 122 println("MediaDeviceManager state:") 123 entries.forEach { (key, entry) -> 124 println(" key=$key") 125 entry.dump(pw) 126 } 127 } 128 } 129 130 @MainThread 131 private fun processDevice(key: String, oldKey: String?, device: MediaDeviceData?) { 132 listeners.forEach { it.onMediaDeviceChanged(key, oldKey, device) } 133 } 134 135 interface Listener { 136 /** Called when the route has changed for a given notification. */ 137 fun onMediaDeviceChanged(key: String, oldKey: String?, data: MediaDeviceData?) 138 /** Called when the notification was removed. */ 139 fun onKeyRemoved(key: String) 140 } 141 142 private inner class Entry( 143 val key: String, 144 val oldKey: String?, 145 val controller: MediaController?, 146 val localMediaManager: LocalMediaManager, 147 val muteAwaitConnectionManager: MediaMuteAwaitConnectionManager, 148 ) : 149 LocalMediaManager.DeviceCallback, 150 MediaController.Callback(), 151 BluetoothLeBroadcast.Callback { 152 153 val token 154 get() = controller?.sessionToken 155 private var started = false 156 private var playbackType = PLAYBACK_TYPE_UNKNOWN 157 private var playbackVolumeControlId: String? = null 158 private var current: MediaDeviceData? = null 159 set(value) { 160 val sameWithoutIcon = value != null && value.equalsWithoutIcon(field) 161 if (!started || !sameWithoutIcon) { 162 field = value 163 fgExecutor.execute { processDevice(key, oldKey, value) } 164 } 165 } 166 // A device that is not yet connected but is expected to connect imminently. Because it's 167 // expected to connect imminently, it should be displayed as the current device. 168 private var aboutToConnectDeviceOverride: AboutToConnectDevice? = null 169 private var broadcastDescription: String? = null 170 private val configListener = 171 object : ConfigurationController.ConfigurationListener { 172 override fun onLocaleListChanged() { 173 updateCurrent() 174 } 175 } 176 177 @AnyThread 178 fun start() = 179 bgExecutor.execute { 180 if (!started) { 181 localMediaManager.registerCallback(this) 182 localMediaManager.startScan() 183 muteAwaitConnectionManager.startListening() 184 playbackType = controller?.playbackInfo?.playbackType ?: PLAYBACK_TYPE_UNKNOWN 185 playbackVolumeControlId = controller?.playbackInfo?.volumeControlId 186 controller?.registerCallback(this) 187 updateCurrent() 188 started = true 189 configurationController.addCallback(configListener) 190 } 191 } 192 193 @AnyThread 194 fun stop() = 195 bgExecutor.execute { 196 if (started) { 197 started = false 198 controller?.unregisterCallback(this) 199 localMediaManager.stopScan() 200 localMediaManager.unregisterCallback(this) 201 muteAwaitConnectionManager.stopListening() 202 configurationController.removeCallback(configListener) 203 } 204 } 205 206 fun dump(pw: PrintWriter) { 207 val routingSession = 208 controller?.let { mr2manager.getRoutingSessionForMediaController(it) } 209 val selectedRoutes = routingSession?.let { mr2manager.getSelectedRoutes(it) } 210 with(pw) { 211 println(" current device is ${current?.name}") 212 val type = controller?.playbackInfo?.playbackType 213 println(" PlaybackType=$type (1 for local, 2 for remote) cached=$playbackType") 214 val volumeControlId = controller?.playbackInfo?.volumeControlId 215 println(" volumeControlId=$volumeControlId cached= $playbackVolumeControlId") 216 println(" routingSession=$routingSession") 217 println(" selectedRoutes=$selectedRoutes") 218 } 219 } 220 221 @WorkerThread 222 override fun onAudioInfoChanged(info: MediaController.PlaybackInfo?) { 223 val newPlaybackType = info?.playbackType ?: PLAYBACK_TYPE_UNKNOWN 224 val newPlaybackVolumeControlId = info?.volumeControlId 225 if ( 226 newPlaybackType == playbackType && 227 newPlaybackVolumeControlId == playbackVolumeControlId 228 ) { 229 return 230 } 231 playbackType = newPlaybackType 232 playbackVolumeControlId = newPlaybackVolumeControlId 233 updateCurrent() 234 } 235 236 override fun onDeviceListUpdate(devices: List<MediaDevice>?) = 237 bgExecutor.execute { updateCurrent() } 238 239 override fun onSelectedDeviceStateChanged(device: MediaDevice, state: Int) { 240 bgExecutor.execute { updateCurrent() } 241 } 242 243 override fun onAboutToConnectDeviceAdded( 244 deviceAddress: String, 245 deviceName: String, 246 deviceIcon: Drawable? 247 ) { 248 aboutToConnectDeviceOverride = 249 AboutToConnectDevice( 250 fullMediaDevice = localMediaManager.getMediaDeviceById(deviceAddress), 251 backupMediaDeviceData = 252 MediaDeviceData( 253 /* enabled */ enabled = true, 254 /* icon */ deviceIcon, 255 /* name */ deviceName, 256 /* showBroadcastButton */ showBroadcastButton = false 257 ) 258 ) 259 updateCurrent() 260 } 261 262 override fun onAboutToConnectDeviceRemoved() { 263 aboutToConnectDeviceOverride = null 264 updateCurrent() 265 } 266 267 override fun onBroadcastStarted(reason: Int, broadcastId: Int) { 268 if (DEBUG) { 269 Log.d(TAG, "onBroadcastStarted(), reason = $reason , broadcastId = $broadcastId") 270 } 271 updateCurrent() 272 } 273 274 override fun onBroadcastStartFailed(reason: Int) { 275 if (DEBUG) { 276 Log.d(TAG, "onBroadcastStartFailed(), reason = $reason") 277 } 278 } 279 280 override fun onBroadcastMetadataChanged( 281 broadcastId: Int, 282 metadata: BluetoothLeBroadcastMetadata 283 ) { 284 if (DEBUG) { 285 Log.d( 286 TAG, 287 "onBroadcastMetadataChanged(), broadcastId = $broadcastId , " + 288 "metadata = $metadata" 289 ) 290 } 291 updateCurrent() 292 } 293 294 override fun onBroadcastStopped(reason: Int, broadcastId: Int) { 295 if (DEBUG) { 296 Log.d(TAG, "onBroadcastStopped(), reason = $reason , broadcastId = $broadcastId") 297 } 298 updateCurrent() 299 } 300 301 override fun onBroadcastStopFailed(reason: Int) { 302 if (DEBUG) { 303 Log.d(TAG, "onBroadcastStopFailed(), reason = $reason") 304 } 305 } 306 307 override fun onBroadcastUpdated(reason: Int, broadcastId: Int) { 308 if (DEBUG) { 309 Log.d(TAG, "onBroadcastUpdated(), reason = $reason , broadcastId = $broadcastId") 310 } 311 updateCurrent() 312 } 313 314 override fun onBroadcastUpdateFailed(reason: Int, broadcastId: Int) { 315 if (DEBUG) { 316 Log.d( 317 TAG, 318 "onBroadcastUpdateFailed(), reason = $reason , " + "broadcastId = $broadcastId" 319 ) 320 } 321 } 322 323 override fun onPlaybackStarted(reason: Int, broadcastId: Int) {} 324 325 override fun onPlaybackStopped(reason: Int, broadcastId: Int) {} 326 327 @WorkerThread 328 private fun updateCurrent() { 329 if (isLeAudioBroadcastEnabled()) { 330 current = 331 MediaDeviceData( 332 /* enabled */ true, 333 /* icon */ context.getDrawable(R.drawable.settings_input_antenna), 334 /* name */ broadcastDescription, 335 /* intent */ null, 336 /* showBroadcastButton */ showBroadcastButton = true 337 ) 338 } else { 339 val aboutToConnect = aboutToConnectDeviceOverride 340 if ( 341 aboutToConnect != null && 342 aboutToConnect.fullMediaDevice == null && 343 aboutToConnect.backupMediaDeviceData != null 344 ) { 345 // Only use [backupMediaDeviceData] when we don't have [fullMediaDevice]. 346 current = aboutToConnect.backupMediaDeviceData 347 return 348 } 349 val device = 350 aboutToConnect?.fullMediaDevice ?: localMediaManager.currentConnectedDevice 351 val route = controller?.let { mr2manager.getRoutingSessionForMediaController(it) } 352 353 // If we have a controller but get a null route, then don't trust the device 354 val enabled = device != null && (controller == null || route != null) 355 val name = 356 if (controller == null || route != null) { 357 route?.name?.toString() ?: device?.name 358 } else { 359 null 360 } 361 current = 362 MediaDeviceData( 363 enabled, 364 device?.iconWithoutBackground, 365 name, 366 id = device?.id, 367 showBroadcastButton = false 368 ) 369 } 370 } 371 372 private fun isLeAudioBroadcastEnabled(): Boolean { 373 if (localBluetoothManager != null) { 374 val profileManager = localBluetoothManager.profileManager 375 if (profileManager != null) { 376 val bluetoothLeBroadcast = profileManager.leAudioBroadcastProfile 377 if (bluetoothLeBroadcast != null && bluetoothLeBroadcast.isEnabled(null)) { 378 getBroadcastingInfo(bluetoothLeBroadcast) 379 return true 380 } else if (DEBUG) { 381 Log.d(TAG, "Can not get LocalBluetoothLeBroadcast") 382 } 383 } else if (DEBUG) { 384 Log.d(TAG, "Can not get LocalBluetoothProfileManager") 385 } 386 } else if (DEBUG) { 387 Log.d(TAG, "Can not get LocalBluetoothManager") 388 } 389 return false 390 } 391 392 private fun getBroadcastingInfo(bluetoothLeBroadcast: LocalBluetoothLeBroadcast) { 393 var currentBroadcastedApp = bluetoothLeBroadcast.appSourceName 394 // TODO(b/233698402): Use the package name instead of app label to avoid the 395 // unexpected result. 396 // Check the current media app's name is the same with current broadcast app's name 397 // or not. 398 var mediaApp = 399 MediaDataUtils.getAppLabel( 400 context, 401 localMediaManager.packageName, 402 context.getString(R.string.bt_le_audio_broadcast_dialog_unknown_name) 403 ) 404 var isCurrentBroadcastedApp = TextUtils.equals(mediaApp, currentBroadcastedApp) 405 if (isCurrentBroadcastedApp) { 406 broadcastDescription = 407 context.getString(R.string.broadcasting_description_is_broadcasting) 408 } else { 409 broadcastDescription = currentBroadcastedApp 410 } 411 } 412 } 413 } 414 415 /** 416 * A class storing information for the about-to-connect device. See 417 * [LocalMediaManager.DeviceCallback.onAboutToConnectDeviceAdded] for more information. 418 * 419 * @property fullMediaDevice a full-fledged [MediaDevice] object representing the device. If 420 * non-null, prefer using [fullMediaDevice] over [backupMediaDeviceData]. 421 * @property backupMediaDeviceData a backup [MediaDeviceData] object containing the minimum 422 * information required to display the device. Only use if [fullMediaDevice] is null. 423 */ 424 private data class AboutToConnectDevice( 425 val fullMediaDevice: MediaDevice? = null, 426 val backupMediaDeviceData: MediaDeviceData? = null 427 ) 428