• 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.pipeline
18 
19 import android.bluetooth.BluetoothLeBroadcast
20 import android.bluetooth.BluetoothLeBroadcastMetadata
21 import android.content.Context
22 import android.graphics.drawable.Drawable
23 import android.media.MediaRouter2Manager
24 import android.media.session.MediaController
25 import android.text.TextUtils
26 import android.util.Log
27 import androidx.annotation.AnyThread
28 import androidx.annotation.MainThread
29 import androidx.annotation.WorkerThread
30 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
31 import com.android.settingslib.bluetooth.LocalBluetoothManager
32 import com.android.settingslib.media.LocalMediaManager
33 import com.android.settingslib.media.MediaDevice
34 import com.android.systemui.Dumpable
35 import com.android.systemui.R
36 import com.android.systemui.dagger.qualifiers.Background
37 import com.android.systemui.dagger.qualifiers.Main
38 import com.android.systemui.dump.DumpManager
39 import com.android.systemui.media.controls.models.player.MediaData
40 import com.android.systemui.media.controls.models.player.MediaDeviceData
41 import com.android.systemui.media.controls.util.MediaControllerFactory
42 import com.android.systemui.media.controls.util.MediaDataUtils
43 import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManager
44 import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManagerFactory
45 import com.android.systemui.statusbar.policy.ConfigurationController
46 import java.io.PrintWriter
47 import java.util.concurrent.Executor
48 import javax.inject.Inject
49 
50 private const val PLAYBACK_TYPE_UNKNOWN = 0
51 private const val TAG = "MediaDeviceManager"
52 private const val DEBUG = true
53 
54 /** Provides information about the route (ie. device) where playback is occurring. */
55 class MediaDeviceManager
56 @Inject
57 constructor(
58     private val context: Context,
59     private val controllerFactory: MediaControllerFactory,
60     private val localMediaManagerFactory: LocalMediaManagerFactory,
61     private val mr2manager: MediaRouter2Manager,
62     private val muteAwaitConnectionManagerFactory: MediaMuteAwaitConnectionManagerFactory,
63     private val configurationController: ConfigurationController,
64     private val localBluetoothManager: LocalBluetoothManager?,
65     @Main private val fgExecutor: Executor,
66     @Background private val bgExecutor: Executor,
67     dumpManager: DumpManager
68 ) : MediaDataManager.Listener, Dumpable {
69 
70     private val listeners: MutableSet<Listener> = mutableSetOf()
71     private val entries: MutableMap<String, Entry> = mutableMapOf()
72 
73     init {
74         dumpManager.registerDumpable(javaClass.name, this)
75     }
76 
77     /** Add a listener for changes to the media route (ie. device). */
78     fun addListener(listener: Listener) = listeners.add(listener)
79 
80     /** Remove a listener that has been registered with addListener. */
81     fun removeListener(listener: Listener) = listeners.remove(listener)
82 
83     override fun onMediaDataLoaded(
84         key: String,
85         oldKey: String?,
86         data: MediaData,
87         immediately: Boolean,
88         receivedSmartspaceCardLatency: Int,
89         isSsReactivated: Boolean
90     ) {
91         if (oldKey != null && oldKey != key) {
92             val oldEntry = entries.remove(oldKey)
93             oldEntry?.stop()
94         }
95         var entry = entries[key]
96         if (entry == null || entry.token != data.token) {
97             entry?.stop()
98             if (data.device != null) {
99                 // If we were already provided device info (e.g. from RCN), keep that and don't
100                 // listen for updates, but process once to push updates to listeners
101                 processDevice(key, oldKey, data.device)
102                 return
103             }
104             val controller = data.token?.let { controllerFactory.create(it) }
105             val localMediaManager = localMediaManagerFactory.create(data.packageName)
106             val muteAwaitConnectionManager =
107                 muteAwaitConnectionManagerFactory.create(localMediaManager)
108             entry = Entry(key, oldKey, controller, localMediaManager, muteAwaitConnectionManager)
109             entries[key] = entry
110             entry.start()
111         }
112     }
113 
114     override fun onMediaDataRemoved(key: String) {
115         val token = entries.remove(key)
116         token?.stop()
117         token?.let { listeners.forEach { it.onKeyRemoved(key) } }
118     }
119 
120     override fun dump(pw: PrintWriter, args: Array<String>) {
121         with(pw) {
122             println("MediaDeviceManager state:")
123             entries.forEach { (key, entry) ->
124                 println("  key=$key")
125                 entry.dump(pw)
126             }
127         }
128     }
129 
130     @MainThread
131     private fun processDevice(key: String, oldKey: String?, device: MediaDeviceData?) {
132         listeners.forEach { it.onMediaDeviceChanged(key, oldKey, device) }
133     }
134 
135     interface Listener {
136         /** Called when the route has changed for a given notification. */
137         fun onMediaDeviceChanged(key: String, oldKey: String?, data: MediaDeviceData?)
138         /** Called when the notification was removed. */
139         fun onKeyRemoved(key: String)
140     }
141 
142     private inner class Entry(
143         val key: String,
144         val oldKey: String?,
145         val controller: MediaController?,
146         val localMediaManager: LocalMediaManager,
147         val muteAwaitConnectionManager: MediaMuteAwaitConnectionManager?
148     ) :
149         LocalMediaManager.DeviceCallback,
150         MediaController.Callback(),
151         BluetoothLeBroadcast.Callback {
152 
153         val token
154             get() = controller?.sessionToken
155         private var started = false
156         private var playbackType = PLAYBACK_TYPE_UNKNOWN
157         private var current: MediaDeviceData? = null
158             set(value) {
159                 val sameWithoutIcon = value != null && value.equalsWithoutIcon(field)
160                 if (!started || !sameWithoutIcon) {
161                     field = value
162                     fgExecutor.execute { processDevice(key, oldKey, value) }
163                 }
164             }
165         // A device that is not yet connected but is expected to connect imminently. Because it's
166         // expected to connect imminently, it should be displayed as the current device.
167         private var aboutToConnectDeviceOverride: AboutToConnectDevice? = null
168         private var broadcastDescription: String? = null
169         private val configListener =
170             object : ConfigurationController.ConfigurationListener {
171                 override fun onLocaleListChanged() {
172                     updateCurrent()
173                 }
174             }
175 
176         @AnyThread
177         fun start() =
178             bgExecutor.execute {
179                 if (!started) {
180                     localMediaManager.registerCallback(this)
181                     localMediaManager.startScan()
182                     muteAwaitConnectionManager?.startListening()
183                     playbackType = controller?.playbackInfo?.playbackType ?: PLAYBACK_TYPE_UNKNOWN
184                     controller?.registerCallback(this)
185                     updateCurrent()
186                     started = true
187                     configurationController.addCallback(configListener)
188                 }
189             }
190 
191         @AnyThread
192         fun stop() =
193             bgExecutor.execute {
194                 if (started) {
195                     started = false
196                     controller?.unregisterCallback(this)
197                     localMediaManager.stopScan()
198                     localMediaManager.unregisterCallback(this)
199                     muteAwaitConnectionManager?.stopListening()
200                     configurationController.removeCallback(configListener)
201                 }
202             }
203 
204         fun dump(pw: PrintWriter) {
205             val routingSession =
206                 controller?.let { mr2manager.getRoutingSessionForMediaController(it) }
207             val selectedRoutes = routingSession?.let { mr2manager.getSelectedRoutes(it) }
208             with(pw) {
209                 println("    current device is ${current?.name}")
210                 val type = controller?.playbackInfo?.playbackType
211                 println("    PlaybackType=$type (1 for local, 2 for remote) cached=$playbackType")
212                 println("    routingSession=$routingSession")
213                 println("    selectedRoutes=$selectedRoutes")
214             }
215         }
216 
217         @WorkerThread
218         override fun onAudioInfoChanged(info: MediaController.PlaybackInfo?) {
219             val newPlaybackType = info?.playbackType ?: PLAYBACK_TYPE_UNKNOWN
220             if (newPlaybackType == playbackType) {
221                 return
222             }
223             playbackType = newPlaybackType
224             updateCurrent()
225         }
226 
227         override fun onDeviceListUpdate(devices: List<MediaDevice>?) =
228             bgExecutor.execute { updateCurrent() }
229 
230         override fun onSelectedDeviceStateChanged(device: MediaDevice, state: Int) {
231             bgExecutor.execute { updateCurrent() }
232         }
233 
234         override fun onAboutToConnectDeviceAdded(
235             deviceAddress: String,
236             deviceName: String,
237             deviceIcon: Drawable?
238         ) {
239             aboutToConnectDeviceOverride =
240                 AboutToConnectDevice(
241                     fullMediaDevice = localMediaManager.getMediaDeviceById(deviceAddress),
242                     backupMediaDeviceData =
243                         MediaDeviceData(
244                             /* enabled */ enabled = true,
245                             /* icon */ deviceIcon,
246                             /* name */ deviceName,
247                             /* showBroadcastButton */ showBroadcastButton = false
248                         )
249                 )
250             updateCurrent()
251         }
252 
253         override fun onAboutToConnectDeviceRemoved() {
254             aboutToConnectDeviceOverride = null
255             updateCurrent()
256         }
257 
258         override fun onBroadcastStarted(reason: Int, broadcastId: Int) {
259             if (DEBUG) {
260                 Log.d(TAG, "onBroadcastStarted(), reason = $reason , broadcastId = $broadcastId")
261             }
262             updateCurrent()
263         }
264 
265         override fun onBroadcastStartFailed(reason: Int) {
266             if (DEBUG) {
267                 Log.d(TAG, "onBroadcastStartFailed(), reason = $reason")
268             }
269         }
270 
271         override fun onBroadcastMetadataChanged(
272             broadcastId: Int,
273             metadata: BluetoothLeBroadcastMetadata
274         ) {
275             if (DEBUG) {
276                 Log.d(
277                     TAG,
278                     "onBroadcastMetadataChanged(), broadcastId = $broadcastId , " +
279                         "metadata = $metadata"
280                 )
281             }
282             updateCurrent()
283         }
284 
285         override fun onBroadcastStopped(reason: Int, broadcastId: Int) {
286             if (DEBUG) {
287                 Log.d(TAG, "onBroadcastStopped(), reason = $reason , broadcastId = $broadcastId")
288             }
289             updateCurrent()
290         }
291 
292         override fun onBroadcastStopFailed(reason: Int) {
293             if (DEBUG) {
294                 Log.d(TAG, "onBroadcastStopFailed(), reason = $reason")
295             }
296         }
297 
298         override fun onBroadcastUpdated(reason: Int, broadcastId: Int) {
299             if (DEBUG) {
300                 Log.d(TAG, "onBroadcastUpdated(), reason = $reason , broadcastId = $broadcastId")
301             }
302             updateCurrent()
303         }
304 
305         override fun onBroadcastUpdateFailed(reason: Int, broadcastId: Int) {
306             if (DEBUG) {
307                 Log.d(
308                     TAG,
309                     "onBroadcastUpdateFailed(), reason = $reason , " + "broadcastId = $broadcastId"
310                 )
311             }
312         }
313 
314         override fun onPlaybackStarted(reason: Int, broadcastId: Int) {}
315 
316         override fun onPlaybackStopped(reason: Int, broadcastId: Int) {}
317 
318         @WorkerThread
319         private fun updateCurrent() {
320             if (isLeAudioBroadcastEnabled()) {
321                 current =
322                     MediaDeviceData(
323                         /* enabled */ true,
324                         /* icon */ context.getDrawable(R.drawable.settings_input_antenna),
325                         /* name */ broadcastDescription,
326                         /* intent */ null,
327                         /* showBroadcastButton */ showBroadcastButton = true
328                     )
329             } else {
330                 val aboutToConnect = aboutToConnectDeviceOverride
331                 if (
332                     aboutToConnect != null &&
333                         aboutToConnect.fullMediaDevice == null &&
334                         aboutToConnect.backupMediaDeviceData != null
335                 ) {
336                     // Only use [backupMediaDeviceData] when we don't have [fullMediaDevice].
337                     current = aboutToConnect.backupMediaDeviceData
338                     return
339                 }
340                 val device =
341                     aboutToConnect?.fullMediaDevice ?: localMediaManager.currentConnectedDevice
342                 val route = controller?.let { mr2manager.getRoutingSessionForMediaController(it) }
343 
344                 // If we have a controller but get a null route, then don't trust the device
345                 val enabled = device != null && (controller == null || route != null)
346                 val name =
347                     if (controller == null || route != null) {
348                         route?.name?.toString() ?: device?.name
349                     } else {
350                         null
351                     }
352                 current =
353                     MediaDeviceData(
354                         enabled,
355                         device?.iconWithoutBackground,
356                         name,
357                         id = device?.id,
358                         showBroadcastButton = false
359                     )
360             }
361         }
362 
363         private fun isLeAudioBroadcastEnabled(): Boolean {
364             if (localBluetoothManager != null) {
365                 val profileManager = localBluetoothManager.profileManager
366                 if (profileManager != null) {
367                     val bluetoothLeBroadcast = profileManager.leAudioBroadcastProfile
368                     if (bluetoothLeBroadcast != null && bluetoothLeBroadcast.isEnabled(null)) {
369                         getBroadcastingInfo(bluetoothLeBroadcast)
370                         return true
371                     } else if (DEBUG) {
372                         Log.d(TAG, "Can not get LocalBluetoothLeBroadcast")
373                     }
374                 } else if (DEBUG) {
375                     Log.d(TAG, "Can not get LocalBluetoothProfileManager")
376                 }
377             } else if (DEBUG) {
378                 Log.d(TAG, "Can not get LocalBluetoothManager")
379             }
380             return false
381         }
382 
383         private fun getBroadcastingInfo(bluetoothLeBroadcast: LocalBluetoothLeBroadcast) {
384             var currentBroadcastedApp = bluetoothLeBroadcast.appSourceName
385             // TODO(b/233698402): Use the package name instead of app label to avoid the
386             // unexpected result.
387             // Check the current media app's name is the same with current broadcast app's name
388             // or not.
389             var mediaApp =
390                 MediaDataUtils.getAppLabel(
391                     context,
392                     localMediaManager.packageName,
393                     context.getString(R.string.bt_le_audio_broadcast_dialog_unknown_name)
394                 )
395             var isCurrentBroadcastedApp = TextUtils.equals(mediaApp, currentBroadcastedApp)
396             if (isCurrentBroadcastedApp) {
397                 broadcastDescription =
398                     context.getString(R.string.broadcasting_description_is_broadcasting)
399             } else {
400                 broadcastDescription = currentBroadcastedApp
401             }
402         }
403     }
404 }
405 
406 /**
407  * A class storing information for the about-to-connect device. See
408  * [LocalMediaManager.DeviceCallback.onAboutToConnectDeviceAdded] for more information.
409  *
410  * @property fullMediaDevice a full-fledged [MediaDevice] object representing the device. If
411  *   non-null, prefer using [fullMediaDevice] over [backupMediaDeviceData].
412  * @property backupMediaDeviceData a backup [MediaDeviceData] object containing the minimum
413  *   information required to display the device. Only use if [fullMediaDevice] is null.
414  */
415 private data class AboutToConnectDevice(
416     val fullMediaDevice: MediaDevice? = null,
417     val backupMediaDeviceData: MediaDeviceData? = null
418 )
419