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(key: String, oldKey: String?, data: MediaData) { 67 if (oldKey != null && oldKey != key) { 68 val oldEntry = entries.remove(oldKey) 69 oldEntry?.stop() 70 } 71 var entry = entries[key] 72 if (entry == null || entry?.token != data.token) { 73 entry?.stop() 74 val controller = data.token?.let { 75 controllerFactory.create(it) 76 } 77 entry = Entry(key, oldKey, controller, 78 localMediaManagerFactory.create(data.packageName)) 79 entries[key] = entry 80 entry.start() 81 } 82 } 83 84 override fun onMediaDataRemoved(key: String) { 85 val token = entries.remove(key) 86 token?.stop() 87 token?.let { 88 listeners.forEach { 89 it.onKeyRemoved(key) 90 } 91 } 92 } 93 94 override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<String>) { 95 with(pw) { 96 println("MediaDeviceManager state:") 97 entries.forEach { 98 key, entry -> 99 println(" key=$key") 100 entry.dump(fd, pw, args) 101 } 102 } 103 } 104 105 @MainThread 106 private fun processDevice(key: String, oldKey: String?, device: MediaDevice?) { 107 val enabled = device != null 108 val data = MediaDeviceData(enabled, device?.iconWithoutBackground, device?.name) 109 listeners.forEach { 110 it.onMediaDeviceChanged(key, oldKey, data) 111 } 112 } 113 114 interface Listener { 115 /** Called when the route has changed for a given notification. */ 116 fun onMediaDeviceChanged(key: String, oldKey: String?, data: MediaDeviceData?) 117 /** Called when the notification was removed. */ 118 fun onKeyRemoved(key: String) 119 } 120 121 private inner class Entry( 122 val key: String, 123 val oldKey: String?, 124 val controller: MediaController?, 125 val localMediaManager: LocalMediaManager 126 ) : LocalMediaManager.DeviceCallback, MediaController.Callback() { 127 128 val token 129 get() = controller?.sessionToken 130 private var started = false 131 private var playbackType = PLAYBACK_TYPE_UNKNOWN 132 private var current: MediaDevice? = null 133 set(value) { 134 if (!started || value != field) { 135 field = value 136 fgExecutor.execute { 137 processDevice(key, oldKey, value) 138 } 139 } 140 } 141 142 @AnyThread 143 fun start() = bgExecutor.execute { 144 localMediaManager.registerCallback(this) 145 localMediaManager.startScan() 146 playbackType = controller?.playbackInfo?.playbackType ?: PLAYBACK_TYPE_UNKNOWN 147 controller?.registerCallback(this) 148 updateCurrent() 149 started = true 150 } 151 152 @AnyThread 153 fun stop() = bgExecutor.execute { 154 started = false 155 controller?.unregisterCallback(this) 156 localMediaManager.stopScan() 157 localMediaManager.unregisterCallback(this) 158 } 159 160 fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<String>) { 161 val routingSession = controller?.let { 162 mr2manager.getRoutingSessionForMediaController(it) 163 } 164 val selectedRoutes = routingSession?.let { 165 mr2manager.getSelectedRoutes(it) 166 } 167 with(pw) { 168 println(" current device is ${current?.name}") 169 val type = controller?.playbackInfo?.playbackType 170 println(" PlaybackType=$type (1 for local, 2 for remote) cached=$playbackType") 171 println(" routingSession=$routingSession") 172 println(" selectedRoutes=$selectedRoutes") 173 } 174 } 175 176 @WorkerThread 177 override fun onAudioInfoChanged(info: MediaController.PlaybackInfo?) { 178 val newPlaybackType = info?.playbackType ?: PLAYBACK_TYPE_UNKNOWN 179 if (newPlaybackType == playbackType) { 180 return 181 } 182 playbackType = newPlaybackType 183 updateCurrent() 184 } 185 186 override fun onDeviceListUpdate(devices: List<MediaDevice>?) = bgExecutor.execute { 187 updateCurrent() 188 } 189 190 override fun onSelectedDeviceStateChanged(device: MediaDevice, state: Int) { 191 bgExecutor.execute { 192 updateCurrent() 193 } 194 } 195 196 @WorkerThread 197 private fun updateCurrent() { 198 val device = localMediaManager.getCurrentConnectedDevice() 199 controller?.let { 200 val route = mr2manager.getRoutingSessionForMediaController(it) 201 // If we get a null route, then don't trust the device. Just set to null to disable the 202 // output switcher chip. 203 current = if (route != null) device else null 204 } ?: run { 205 current = device 206 } 207 } 208 } 209 } 210