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