blob: 120704c0582abe030f212f2bc8be585184dc925d [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.media.controls.pipeline
import android.bluetooth.BluetoothLeBroadcast
import android.bluetooth.BluetoothLeBroadcastMetadata
import android.content.Context
import android.graphics.drawable.Drawable
import android.media.MediaRouter2Manager
import android.media.session.MediaController
import android.text.TextUtils
import android.util.Log
import androidx.annotation.AnyThread
import androidx.annotation.MainThread
import androidx.annotation.WorkerThread
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
import com.android.settingslib.bluetooth.LocalBluetoothManager
import com.android.settingslib.media.LocalMediaManager
import com.android.settingslib.media.MediaDevice
import com.android.systemui.Dumpable
import com.android.systemui.R
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dump.DumpManager
import com.android.systemui.media.controls.models.player.MediaData
import com.android.systemui.media.controls.models.player.MediaDeviceData
import com.android.systemui.media.controls.util.MediaControllerFactory
import com.android.systemui.media.controls.util.MediaDataUtils
import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManager
import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionManagerFactory
import com.android.systemui.statusbar.policy.ConfigurationController
import java.io.PrintWriter
import java.util.concurrent.Executor
import javax.inject.Inject
private const val PLAYBACK_TYPE_UNKNOWN = 0
private const val TAG = "MediaDeviceManager"
private const val DEBUG = true
/** Provides information about the route (ie. device) where playback is occurring. */
class MediaDeviceManager
@Inject
constructor(
private val context: Context,
private val controllerFactory: MediaControllerFactory,
private val localMediaManagerFactory: LocalMediaManagerFactory,
private val mr2manager: MediaRouter2Manager,
private val muteAwaitConnectionManagerFactory: MediaMuteAwaitConnectionManagerFactory,
private val configurationController: ConfigurationController,
private val localBluetoothManager: LocalBluetoothManager?,
@Main private val fgExecutor: Executor,
@Background private val bgExecutor: Executor,
dumpManager: DumpManager
) : MediaDataManager.Listener, Dumpable {
private val listeners: MutableSet<Listener> = mutableSetOf()
private val entries: MutableMap<String, Entry> = mutableMapOf()
init {
dumpManager.registerDumpable(javaClass.name, this)
}
/** Add a listener for changes to the media route (ie. device). */
fun addListener(listener: Listener) = listeners.add(listener)
/** Remove a listener that has been registered with addListener. */
fun removeListener(listener: Listener) = listeners.remove(listener)
override fun onMediaDataLoaded(
key: String,
oldKey: String?,
data: MediaData,
immediately: Boolean,
receivedSmartspaceCardLatency: Int,
isSsReactivated: Boolean
) {
if (oldKey != null && oldKey != key) {
val oldEntry = entries.remove(oldKey)
oldEntry?.stop()
}
var entry = entries[key]
if (entry == null || entry.token != data.token) {
entry?.stop()
if (data.device != null) {
// If we were already provided device info (e.g. from RCN), keep that and don't
// listen for updates, but process once to push updates to listeners
processDevice(key, oldKey, data.device)
return
}
val controller = data.token?.let { controllerFactory.create(it) }
val localMediaManager = localMediaManagerFactory.create(data.packageName)
val muteAwaitConnectionManager =
muteAwaitConnectionManagerFactory.create(localMediaManager)
entry = Entry(key, oldKey, controller, localMediaManager, muteAwaitConnectionManager)
entries[key] = entry
entry.start()
}
}
override fun onMediaDataRemoved(key: String) {
val token = entries.remove(key)
token?.stop()
token?.let { listeners.forEach { it.onKeyRemoved(key) } }
}
override fun dump(pw: PrintWriter, args: Array<String>) {
with(pw) {
println("MediaDeviceManager state:")
entries.forEach { (key, entry) ->
println(" key=$key")
entry.dump(pw)
}
}
}
@MainThread
private fun processDevice(key: String, oldKey: String?, device: MediaDeviceData?) {
listeners.forEach { it.onMediaDeviceChanged(key, oldKey, device) }
}
interface Listener {
/** Called when the route has changed for a given notification. */
fun onMediaDeviceChanged(key: String, oldKey: String?, data: MediaDeviceData?)
/** Called when the notification was removed. */
fun onKeyRemoved(key: String)
}
private inner class Entry(
val key: String,
val oldKey: String?,
val controller: MediaController?,
val localMediaManager: LocalMediaManager,
val muteAwaitConnectionManager: MediaMuteAwaitConnectionManager?
) :
LocalMediaManager.DeviceCallback,
MediaController.Callback(),
BluetoothLeBroadcast.Callback {
val token
get() = controller?.sessionToken
private var started = false
private var playbackType = PLAYBACK_TYPE_UNKNOWN
private var current: MediaDeviceData? = null
set(value) {
val sameWithoutIcon = value != null && value.equalsWithoutIcon(field)
if (!started || !sameWithoutIcon) {
field = value
fgExecutor.execute { processDevice(key, oldKey, value) }
}
}
// A device that is not yet connected but is expected to connect imminently. Because it's
// expected to connect imminently, it should be displayed as the current device.
private var aboutToConnectDeviceOverride: AboutToConnectDevice? = null
private var broadcastDescription: String? = null
private val configListener =
object : ConfigurationController.ConfigurationListener {
override fun onLocaleListChanged() {
updateCurrent()
}
}
@AnyThread
fun start() =
bgExecutor.execute {
if (!started) {
localMediaManager.registerCallback(this)
localMediaManager.startScan()
muteAwaitConnectionManager?.startListening()
playbackType = controller?.playbackInfo?.playbackType ?: PLAYBACK_TYPE_UNKNOWN
controller?.registerCallback(this)
updateCurrent()
started = true
configurationController.addCallback(configListener)
}
}
@AnyThread
fun stop() =
bgExecutor.execute {
if (started) {
started = false
controller?.unregisterCallback(this)
localMediaManager.stopScan()
localMediaManager.unregisterCallback(this)
muteAwaitConnectionManager?.stopListening()
configurationController.removeCallback(configListener)
}
}
fun dump(pw: PrintWriter) {
val routingSession =
controller?.let { mr2manager.getRoutingSessionForMediaController(it) }
val selectedRoutes = routingSession?.let { mr2manager.getSelectedRoutes(it) }
with(pw) {
println(" current device is ${current?.name}")
val type = controller?.playbackInfo?.playbackType
println(" PlaybackType=$type (1 for local, 2 for remote) cached=$playbackType")
println(" routingSession=$routingSession")
println(" selectedRoutes=$selectedRoutes")
}
}
@WorkerThread
override fun onAudioInfoChanged(info: MediaController.PlaybackInfo?) {
val newPlaybackType = info?.playbackType ?: PLAYBACK_TYPE_UNKNOWN
if (newPlaybackType == playbackType) {
return
}
playbackType = newPlaybackType
updateCurrent()
}
override fun onDeviceListUpdate(devices: List<MediaDevice>?) =
bgExecutor.execute { updateCurrent() }
override fun onSelectedDeviceStateChanged(device: MediaDevice, state: Int) {
bgExecutor.execute { updateCurrent() }
}
override fun onAboutToConnectDeviceAdded(
deviceAddress: String,
deviceName: String,
deviceIcon: Drawable?
) {
aboutToConnectDeviceOverride =
AboutToConnectDevice(
fullMediaDevice = localMediaManager.getMediaDeviceById(deviceAddress),
backupMediaDeviceData =
MediaDeviceData(
/* enabled */ enabled = true,
/* icon */ deviceIcon,
/* name */ deviceName,
/* showBroadcastButton */ showBroadcastButton = false
)
)
updateCurrent()
}
override fun onAboutToConnectDeviceRemoved() {
aboutToConnectDeviceOverride = null
updateCurrent()
}
override fun onBroadcastStarted(reason: Int, broadcastId: Int) {
if (DEBUG) {
Log.d(TAG, "onBroadcastStarted(), reason = $reason , broadcastId = $broadcastId")
}
updateCurrent()
}
override fun onBroadcastStartFailed(reason: Int) {
if (DEBUG) {
Log.d(TAG, "onBroadcastStartFailed(), reason = $reason")
}
}
override fun onBroadcastMetadataChanged(
broadcastId: Int,
metadata: BluetoothLeBroadcastMetadata
) {
if (DEBUG) {
Log.d(
TAG,
"onBroadcastMetadataChanged(), broadcastId = $broadcastId , " +
"metadata = $metadata"
)
}
updateCurrent()
}
override fun onBroadcastStopped(reason: Int, broadcastId: Int) {
if (DEBUG) {
Log.d(TAG, "onBroadcastStopped(), reason = $reason , broadcastId = $broadcastId")
}
updateCurrent()
}
override fun onBroadcastStopFailed(reason: Int) {
if (DEBUG) {
Log.d(TAG, "onBroadcastStopFailed(), reason = $reason")
}
}
override fun onBroadcastUpdated(reason: Int, broadcastId: Int) {
if (DEBUG) {
Log.d(TAG, "onBroadcastUpdated(), reason = $reason , broadcastId = $broadcastId")
}
updateCurrent()
}
override fun onBroadcastUpdateFailed(reason: Int, broadcastId: Int) {
if (DEBUG) {
Log.d(
TAG,
"onBroadcastUpdateFailed(), reason = $reason , " + "broadcastId = $broadcastId"
)
}
}
override fun onPlaybackStarted(reason: Int, broadcastId: Int) {}
override fun onPlaybackStopped(reason: Int, broadcastId: Int) {}
@WorkerThread
private fun updateCurrent() {
if (isLeAudioBroadcastEnabled()) {
current =
MediaDeviceData(
/* enabled */ true,
/* icon */ context.getDrawable(R.drawable.settings_input_antenna),
/* name */ broadcastDescription,
/* intent */ null,
/* showBroadcastButton */ showBroadcastButton = true
)
} else {
val aboutToConnect = aboutToConnectDeviceOverride
if (
aboutToConnect != null &&
aboutToConnect.fullMediaDevice == null &&
aboutToConnect.backupMediaDeviceData != null
) {
// Only use [backupMediaDeviceData] when we don't have [fullMediaDevice].
current = aboutToConnect.backupMediaDeviceData
return
}
val device =
aboutToConnect?.fullMediaDevice ?: localMediaManager.currentConnectedDevice
val route = controller?.let { mr2manager.getRoutingSessionForMediaController(it) }
// If we have a controller but get a null route, then don't trust the device
val enabled = device != null && (controller == null || route != null)
val name =
if (controller == null || route != null) {
route?.name?.toString() ?: device?.name
} else {
null
}
current =
MediaDeviceData(
enabled,
device?.iconWithoutBackground,
name,
id = device?.id,
showBroadcastButton = false
)
}
}
private fun isLeAudioBroadcastEnabled(): Boolean {
if (localBluetoothManager != null) {
val profileManager = localBluetoothManager.profileManager
if (profileManager != null) {
val bluetoothLeBroadcast = profileManager.leAudioBroadcastProfile
if (bluetoothLeBroadcast != null && bluetoothLeBroadcast.isEnabled(null)) {
getBroadcastingInfo(bluetoothLeBroadcast)
return true
} else if (DEBUG) {
Log.d(TAG, "Can not get LocalBluetoothLeBroadcast")
}
} else if (DEBUG) {
Log.d(TAG, "Can not get LocalBluetoothProfileManager")
}
} else if (DEBUG) {
Log.d(TAG, "Can not get LocalBluetoothManager")
}
return false
}
private fun getBroadcastingInfo(bluetoothLeBroadcast: LocalBluetoothLeBroadcast) {
var currentBroadcastedApp = bluetoothLeBroadcast.appSourceName
// TODO(b/233698402): Use the package name instead of app label to avoid the
// unexpected result.
// Check the current media app's name is the same with current broadcast app's name
// or not.
var mediaApp =
MediaDataUtils.getAppLabel(
context,
localMediaManager.packageName,
context.getString(R.string.bt_le_audio_broadcast_dialog_unknown_name)
)
var isCurrentBroadcastedApp = TextUtils.equals(mediaApp, currentBroadcastedApp)
if (isCurrentBroadcastedApp) {
broadcastDescription =
context.getString(R.string.broadcasting_description_is_broadcasting)
} else {
broadcastDescription = currentBroadcastedApp
}
}
}
}
/**
* A class storing information for the about-to-connect device. See
* [LocalMediaManager.DeviceCallback.onAboutToConnectDeviceAdded] for more information.
*
* @property fullMediaDevice a full-fledged [MediaDevice] object representing the device. If
* non-null, prefer using [fullMediaDevice] over [backupMediaDeviceData].
* @property backupMediaDeviceData a backup [MediaDeviceData] object containing the minimum
* information required to display the device. Only use if [fullMediaDevice] is null.
*/
private data class AboutToConnectDevice(
val fullMediaDevice: MediaDevice? = null,
val backupMediaDeviceData: MediaDeviceData? = null
)