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