• 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
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