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 18 19 import android.media.MediaRouter2Manager 20 import android.media.session.MediaController 21 import androidx.annotation.AnyThread 22 import androidx.annotation.MainThread 23 import androidx.annotation.WorkerThread 24 import com.android.settingslib.media.LocalMediaManager 25 import com.android.settingslib.media.MediaDevice 26 import com.android.systemui.Dumpable 27 import com.android.systemui.dagger.qualifiers.Background 28 import com.android.systemui.dagger.qualifiers.Main 29 import com.android.systemui.dump.DumpManager 30 import java.io.FileDescriptor 31 import java.io.PrintWriter 32 import java.util.concurrent.Executor 33 import javax.inject.Inject 34 35 private const val PLAYBACK_TYPE_UNKNOWN = 0 36 37 /** 38 * Provides information about the route (ie. device) where playback is occurring. 39 */ 40 class MediaDeviceManager @Inject constructor( 41 private val controllerFactory: MediaControllerFactory, 42 private val localMediaManagerFactory: LocalMediaManagerFactory, 43 private val mr2manager: MediaRouter2Manager, 44 @Main private val fgExecutor: Executor, 45 @Background private val bgExecutor: Executor, 46 dumpManager: DumpManager 47 ) : MediaDataManager.Listener, Dumpable { 48 49 private val listeners: MutableSet<Listener> = mutableSetOf() 50 private val entries: MutableMap<String, Entry> = mutableMapOf() 51 52 init { 53 dumpManager.registerDumpable(javaClass.name, this) 54 } 55 56 /** 57 * Add a listener for changes to the media route (ie. device). 58 */ 59 fun addListener(listener: Listener) = listeners.add(listener) 60 61 /** 62 * Remove a listener that has been registered with addListener. 63 */ 64 fun removeListener(listener: Listener) = listeners.remove(listener) 65 66 override fun onMediaDataLoaded( 67 key: String, 68 oldKey: String?, 69 data: MediaData, 70 immediately: Boolean, 71 receivedSmartspaceCardLatency: Int 72 ) { 73 if (oldKey != null && oldKey != key) { 74 val oldEntry = entries.remove(oldKey) 75 oldEntry?.stop() 76 } 77 var entry = entries[key] 78 if (entry == null || entry?.token != data.token) { 79 entry?.stop() 80 val controller = data.token?.let { 81 controllerFactory.create(it) 82 } 83 entry = Entry(key, oldKey, controller, 84 localMediaManagerFactory.create(data.packageName)) 85 entries[key] = entry 86 entry.start() 87 } 88 } 89 90 override fun onMediaDataRemoved(key: String) { 91 val token = entries.remove(key) 92 token?.stop() 93 token?.let { 94 listeners.forEach { 95 it.onKeyRemoved(key) 96 } 97 } 98 } 99 100 override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<String>) { 101 with(pw) { 102 println("MediaDeviceManager state:") 103 entries.forEach { 104 key, entry -> 105 println(" key=$key") 106 entry.dump(fd, pw, args) 107 } 108 } 109 } 110 111 @MainThread 112 private fun processDevice(key: String, oldKey: String?, device: MediaDeviceData?) { 113 listeners.forEach { 114 it.onMediaDeviceChanged(key, oldKey, device) 115 } 116 } 117 118 interface Listener { 119 /** Called when the route has changed for a given notification. */ 120 fun onMediaDeviceChanged(key: String, oldKey: String?, data: MediaDeviceData?) 121 /** Called when the notification was removed. */ 122 fun onKeyRemoved(key: String) 123 } 124 125 private inner class Entry( 126 val key: String, 127 val oldKey: String?, 128 val controller: MediaController?, 129 val localMediaManager: LocalMediaManager 130 ) : LocalMediaManager.DeviceCallback, MediaController.Callback() { 131 132 val token 133 get() = controller?.sessionToken 134 private var started = false 135 private var playbackType = PLAYBACK_TYPE_UNKNOWN 136 private var current: MediaDeviceData? = null 137 set(value) { 138 if (!started || value != field) { 139 field = value 140 fgExecutor.execute { 141 processDevice(key, oldKey, value) 142 } 143 } 144 } 145 146 @AnyThread 147 fun start() = bgExecutor.execute { 148 localMediaManager.registerCallback(this) 149 localMediaManager.startScan() 150 playbackType = controller?.playbackInfo?.playbackType ?: PLAYBACK_TYPE_UNKNOWN 151 controller?.registerCallback(this) 152 updateCurrent() 153 started = true 154 } 155 156 @AnyThread 157 fun stop() = bgExecutor.execute { 158 started = false 159 controller?.unregisterCallback(this) 160 localMediaManager.stopScan() 161 localMediaManager.unregisterCallback(this) 162 } 163 164 fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<String>) { 165 val routingSession = controller?.let { 166 mr2manager.getRoutingSessionForMediaController(it) 167 } 168 val selectedRoutes = routingSession?.let { 169 mr2manager.getSelectedRoutes(it) 170 } 171 with(pw) { 172 println(" current device is ${current?.name}") 173 val type = controller?.playbackInfo?.playbackType 174 println(" PlaybackType=$type (1 for local, 2 for remote) cached=$playbackType") 175 println(" routingSession=$routingSession") 176 println(" selectedRoutes=$selectedRoutes") 177 } 178 } 179 180 @WorkerThread 181 override fun onAudioInfoChanged(info: MediaController.PlaybackInfo?) { 182 val newPlaybackType = info?.playbackType ?: PLAYBACK_TYPE_UNKNOWN 183 if (newPlaybackType == playbackType) { 184 return 185 } 186 playbackType = newPlaybackType 187 updateCurrent() 188 } 189 190 override fun onDeviceListUpdate(devices: List<MediaDevice>?) = bgExecutor.execute { 191 updateCurrent() 192 } 193 194 override fun onSelectedDeviceStateChanged(device: MediaDevice, state: Int) { 195 bgExecutor.execute { 196 updateCurrent() 197 } 198 } 199 200 @WorkerThread 201 private fun updateCurrent() { 202 val device = localMediaManager.currentConnectedDevice 203 val route = controller?.let { mr2manager.getRoutingSessionForMediaController(it) } 204 205 // If we have a controller but get a null route, then don't trust the device 206 val enabled = device != null && (controller == null || route != null) 207 val name = route?.name?.toString() ?: device?.name 208 current = MediaDeviceData(enabled, device?.iconWithoutBackground, name) 209 } 210 } 211 } 212