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