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 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 isSsReactivated: Boolean 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: MediaDevice?) { 113 val enabled = device != null 114 val data = MediaDeviceData(enabled, device?.iconWithoutBackground, device?.name) 115 listeners.forEach { 116 it.onMediaDeviceChanged(key, oldKey, data) 117 } 118 } 119 120 interface Listener { 121 /** Called when the route has changed for a given notification. */ 122 fun onMediaDeviceChanged(key: String, oldKey: String?, data: MediaDeviceData?) 123 /** Called when the notification was removed. */ 124 fun onKeyRemoved(key: String) 125 } 126 127 private inner class Entry( 128 val key: String, 129 val oldKey: String?, 130 val controller: MediaController?, 131 val localMediaManager: LocalMediaManager 132 ) : LocalMediaManager.DeviceCallback, MediaController.Callback() { 133 134 val token 135 get() = controller?.sessionToken 136 private var started = false 137 private var playbackType = PLAYBACK_TYPE_UNKNOWN 138 private var current: MediaDevice? = null 139 set(value) { 140 if (!started || value != field) { 141 field = value 142 fgExecutor.execute { 143 processDevice(key, oldKey, value) 144 } 145 } 146 } 147 148 @AnyThread 149 fun start() = bgExecutor.execute { 150 localMediaManager.registerCallback(this) 151 localMediaManager.startScan() 152 playbackType = controller?.playbackInfo?.playbackType ?: PLAYBACK_TYPE_UNKNOWN 153 controller?.registerCallback(this) 154 updateCurrent() 155 started = true 156 } 157 158 @AnyThread 159 fun stop() = bgExecutor.execute { 160 started = false 161 controller?.unregisterCallback(this) 162 localMediaManager.stopScan() 163 localMediaManager.unregisterCallback(this) 164 } 165 166 fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<String>) { 167 val routingSession = controller?.let { 168 mr2manager.getRoutingSessionForMediaController(it) 169 } 170 val selectedRoutes = routingSession?.let { 171 mr2manager.getSelectedRoutes(it) 172 } 173 with(pw) { 174 println(" current device is ${current?.name}") 175 val type = controller?.playbackInfo?.playbackType 176 println(" PlaybackType=$type (1 for local, 2 for remote) cached=$playbackType") 177 println(" routingSession=$routingSession") 178 println(" selectedRoutes=$selectedRoutes") 179 } 180 } 181 182 @WorkerThread 183 override fun onAudioInfoChanged(info: MediaController.PlaybackInfo?) { 184 val newPlaybackType = info?.playbackType ?: PLAYBACK_TYPE_UNKNOWN 185 if (newPlaybackType == playbackType) { 186 return 187 } 188 playbackType = newPlaybackType 189 updateCurrent() 190 } 191 192 override fun onDeviceListUpdate(devices: List<MediaDevice>?) = bgExecutor.execute { 193 updateCurrent() 194 } 195 196 override fun onSelectedDeviceStateChanged(device: MediaDevice, state: Int) { 197 bgExecutor.execute { 198 updateCurrent() 199 } 200 } 201 202 @WorkerThread 203 private fun updateCurrent() { 204 val device = localMediaManager.getCurrentConnectedDevice() 205 controller?.let { 206 val route = mr2manager.getRoutingSessionForMediaController(it) 207 // If we get a null route, then don't trust the device. Just set to null to disable the 208 // output switcher chip. 209 current = if (route != null) device else null 210 } ?: run { 211 current = device 212 } 213 } 214 } 215 } 216