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