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