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