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.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 current: MediaDeviceData? = null 158 set(value) { 159 val sameWithoutIcon = value != null && value.equalsWithoutIcon(field) 160 if (!started || !sameWithoutIcon) { 161 field = value 162 fgExecutor.execute { processDevice(key, oldKey, value) } 163 } 164 } 165 // A device that is not yet connected but is expected to connect imminently. Because it's 166 // expected to connect imminently, it should be displayed as the current device. 167 private var aboutToConnectDeviceOverride: AboutToConnectDevice? = null 168 private var broadcastDescription: String? = null 169 private val configListener = 170 object : ConfigurationController.ConfigurationListener { 171 override fun onLocaleListChanged() { 172 updateCurrent() 173 } 174 } 175 176 @AnyThread 177 fun start() = 178 bgExecutor.execute { 179 if (!started) { 180 localMediaManager.registerCallback(this) 181 localMediaManager.startScan() 182 muteAwaitConnectionManager?.startListening() 183 playbackType = controller?.playbackInfo?.playbackType ?: PLAYBACK_TYPE_UNKNOWN 184 controller?.registerCallback(this) 185 updateCurrent() 186 started = true 187 configurationController.addCallback(configListener) 188 } 189 } 190 191 @AnyThread 192 fun stop() = 193 bgExecutor.execute { 194 if (started) { 195 started = false 196 controller?.unregisterCallback(this) 197 localMediaManager.stopScan() 198 localMediaManager.unregisterCallback(this) 199 muteAwaitConnectionManager?.stopListening() 200 configurationController.removeCallback(configListener) 201 } 202 } 203 204 fun dump(pw: PrintWriter) { 205 val routingSession = 206 controller?.let { mr2manager.getRoutingSessionForMediaController(it) } 207 val selectedRoutes = routingSession?.let { mr2manager.getSelectedRoutes(it) } 208 with(pw) { 209 println(" current device is ${current?.name}") 210 val type = controller?.playbackInfo?.playbackType 211 println(" PlaybackType=$type (1 for local, 2 for remote) cached=$playbackType") 212 println(" routingSession=$routingSession") 213 println(" selectedRoutes=$selectedRoutes") 214 } 215 } 216 217 @WorkerThread 218 override fun onAudioInfoChanged(info: MediaController.PlaybackInfo?) { 219 val newPlaybackType = info?.playbackType ?: PLAYBACK_TYPE_UNKNOWN 220 if (newPlaybackType == playbackType) { 221 return 222 } 223 playbackType = newPlaybackType 224 updateCurrent() 225 } 226 227 override fun onDeviceListUpdate(devices: List<MediaDevice>?) = 228 bgExecutor.execute { updateCurrent() } 229 230 override fun onSelectedDeviceStateChanged(device: MediaDevice, state: Int) { 231 bgExecutor.execute { updateCurrent() } 232 } 233 234 override fun onAboutToConnectDeviceAdded( 235 deviceAddress: String, 236 deviceName: String, 237 deviceIcon: Drawable? 238 ) { 239 aboutToConnectDeviceOverride = 240 AboutToConnectDevice( 241 fullMediaDevice = localMediaManager.getMediaDeviceById(deviceAddress), 242 backupMediaDeviceData = 243 MediaDeviceData( 244 /* enabled */ enabled = true, 245 /* icon */ deviceIcon, 246 /* name */ deviceName, 247 /* showBroadcastButton */ showBroadcastButton = false 248 ) 249 ) 250 updateCurrent() 251 } 252 253 override fun onAboutToConnectDeviceRemoved() { 254 aboutToConnectDeviceOverride = null 255 updateCurrent() 256 } 257 258 override fun onBroadcastStarted(reason: Int, broadcastId: Int) { 259 if (DEBUG) { 260 Log.d(TAG, "onBroadcastStarted(), reason = $reason , broadcastId = $broadcastId") 261 } 262 updateCurrent() 263 } 264 265 override fun onBroadcastStartFailed(reason: Int) { 266 if (DEBUG) { 267 Log.d(TAG, "onBroadcastStartFailed(), reason = $reason") 268 } 269 } 270 271 override fun onBroadcastMetadataChanged( 272 broadcastId: Int, 273 metadata: BluetoothLeBroadcastMetadata 274 ) { 275 if (DEBUG) { 276 Log.d( 277 TAG, 278 "onBroadcastMetadataChanged(), broadcastId = $broadcastId , " + 279 "metadata = $metadata" 280 ) 281 } 282 updateCurrent() 283 } 284 285 override fun onBroadcastStopped(reason: Int, broadcastId: Int) { 286 if (DEBUG) { 287 Log.d(TAG, "onBroadcastStopped(), reason = $reason , broadcastId = $broadcastId") 288 } 289 updateCurrent() 290 } 291 292 override fun onBroadcastStopFailed(reason: Int) { 293 if (DEBUG) { 294 Log.d(TAG, "onBroadcastStopFailed(), reason = $reason") 295 } 296 } 297 298 override fun onBroadcastUpdated(reason: Int, broadcastId: Int) { 299 if (DEBUG) { 300 Log.d(TAG, "onBroadcastUpdated(), reason = $reason , broadcastId = $broadcastId") 301 } 302 updateCurrent() 303 } 304 305 override fun onBroadcastUpdateFailed(reason: Int, broadcastId: Int) { 306 if (DEBUG) { 307 Log.d( 308 TAG, 309 "onBroadcastUpdateFailed(), reason = $reason , " + "broadcastId = $broadcastId" 310 ) 311 } 312 } 313 314 override fun onPlaybackStarted(reason: Int, broadcastId: Int) {} 315 316 override fun onPlaybackStopped(reason: Int, broadcastId: Int) {} 317 318 @WorkerThread 319 private fun updateCurrent() { 320 if (isLeAudioBroadcastEnabled()) { 321 current = 322 MediaDeviceData( 323 /* enabled */ true, 324 /* icon */ context.getDrawable(R.drawable.settings_input_antenna), 325 /* name */ broadcastDescription, 326 /* intent */ null, 327 /* showBroadcastButton */ showBroadcastButton = true 328 ) 329 } else { 330 val aboutToConnect = aboutToConnectDeviceOverride 331 if ( 332 aboutToConnect != null && 333 aboutToConnect.fullMediaDevice == null && 334 aboutToConnect.backupMediaDeviceData != null 335 ) { 336 // Only use [backupMediaDeviceData] when we don't have [fullMediaDevice]. 337 current = aboutToConnect.backupMediaDeviceData 338 return 339 } 340 val device = 341 aboutToConnect?.fullMediaDevice ?: localMediaManager.currentConnectedDevice 342 val route = controller?.let { mr2manager.getRoutingSessionForMediaController(it) } 343 344 // If we have a controller but get a null route, then don't trust the device 345 val enabled = device != null && (controller == null || route != null) 346 val name = 347 if (controller == null || route != null) { 348 route?.name?.toString() ?: device?.name 349 } else { 350 null 351 } 352 current = 353 MediaDeviceData( 354 enabled, 355 device?.iconWithoutBackground, 356 name, 357 id = device?.id, 358 showBroadcastButton = false 359 ) 360 } 361 } 362 363 private fun isLeAudioBroadcastEnabled(): Boolean { 364 if (localBluetoothManager != null) { 365 val profileManager = localBluetoothManager.profileManager 366 if (profileManager != null) { 367 val bluetoothLeBroadcast = profileManager.leAudioBroadcastProfile 368 if (bluetoothLeBroadcast != null && bluetoothLeBroadcast.isEnabled(null)) { 369 getBroadcastingInfo(bluetoothLeBroadcast) 370 return true 371 } else if (DEBUG) { 372 Log.d(TAG, "Can not get LocalBluetoothLeBroadcast") 373 } 374 } else if (DEBUG) { 375 Log.d(TAG, "Can not get LocalBluetoothProfileManager") 376 } 377 } else if (DEBUG) { 378 Log.d(TAG, "Can not get LocalBluetoothManager") 379 } 380 return false 381 } 382 383 private fun getBroadcastingInfo(bluetoothLeBroadcast: LocalBluetoothLeBroadcast) { 384 var currentBroadcastedApp = bluetoothLeBroadcast.appSourceName 385 // TODO(b/233698402): Use the package name instead of app label to avoid the 386 // unexpected result. 387 // Check the current media app's name is the same with current broadcast app's name 388 // or not. 389 var mediaApp = 390 MediaDataUtils.getAppLabel( 391 context, 392 localMediaManager.packageName, 393 context.getString(R.string.bt_le_audio_broadcast_dialog_unknown_name) 394 ) 395 var isCurrentBroadcastedApp = TextUtils.equals(mediaApp, currentBroadcastedApp) 396 if (isCurrentBroadcastedApp) { 397 broadcastDescription = 398 context.getString(R.string.broadcasting_description_is_broadcasting) 399 } else { 400 broadcastDescription = currentBroadcastedApp 401 } 402 } 403 } 404 } 405 406 /** 407 * A class storing information for the about-to-connect device. See 408 * [LocalMediaManager.DeviceCallback.onAboutToConnectDeviceAdded] for more information. 409 * 410 * @property fullMediaDevice a full-fledged [MediaDevice] object representing the device. If 411 * non-null, prefer using [fullMediaDevice] over [backupMediaDeviceData]. 412 * @property backupMediaDeviceData a backup [MediaDeviceData] object containing the minimum 413 * information required to display the device. Only use if [fullMediaDevice] is null. 414 */ 415 private data class AboutToConnectDevice( 416 val fullMediaDevice: MediaDevice? = null, 417 val backupMediaDeviceData: MediaDeviceData? = null 418 ) 419