1 /*
<lambda>null2 * Copyright 2025 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 package com.android.settingslib.volume
17
18 import android.content.Context
19 import android.content.Intent
20 import android.content.pm.PackageManager
21 import android.media.MediaMetadata
22 import android.media.session.MediaController
23 import android.media.session.MediaController.PlaybackInfo
24 import android.media.session.MediaSession
25 import android.media.session.MediaSessionManager
26 import android.media.session.PlaybackState
27 import android.os.Bundle
28 import android.os.Handler
29 import android.os.HandlerExecutor
30 import android.os.Looper
31 import android.os.Message
32 import android.util.Log
33 import java.io.PrintWriter
34 import java.util.Objects
35
36 /**
37 * Convenience client for all media session updates. Provides a callback interface for events
38 * related to remote media sessions.
39 */
40 class MediaSessions(context: Context, looper: Looper, callbacks: Callbacks) {
41
42 private val mContext = context
43 private val mHandler: H = H(looper)
44 private val mHandlerExecutor: HandlerExecutor = HandlerExecutor(mHandler)
45 private val mMgr: MediaSessionManager =
46 mContext.getSystemService(Context.MEDIA_SESSION_SERVICE) as MediaSessionManager
47 private val mRecords: MutableMap<MediaSession.Token, MediaControllerRecord> = HashMap()
48 private val mCallbacks: Callbacks = callbacks
49 private val mSessionsListener =
50 MediaSessionManager.OnActiveSessionsChangedListener { controllers ->
51 onActiveSessionsUpdatedH(controllers!!)
52 }
53
54 private val mRemoteSessionCallback: MediaSessionManager.RemoteSessionCallback =
55 object : MediaSessionManager.RemoteSessionCallback {
56 override fun onVolumeChanged(sessionToken: MediaSession.Token, flags: Int) {
57 mHandler.obtainMessage(REMOTE_VOLUME_CHANGED, flags, 0, sessionToken).sendToTarget()
58 }
59
60 override fun onDefaultRemoteSessionChanged(sessionToken: MediaSession.Token?) {
61 mHandler.obtainMessage(UPDATE_REMOTE_SESSION_LIST, sessionToken).sendToTarget()
62 }
63 }
64
65 private var mInit = false
66
67 /** Dump to `writer` */
68 fun dump(writer: PrintWriter) {
69 writer.println(javaClass.simpleName + " state:")
70 writer.print(" mInit: ")
71 writer.println(mInit)
72 writer.print(" mRecords.size: ")
73 writer.println(mRecords.size)
74 for ((i, r) in mRecords.values.withIndex()) {
75 r.controller.dump(i + 1, writer)
76 }
77 }
78
79 /** init MediaSessions */
80 fun init() {
81 if (D.BUG) {
82 Log.d(TAG, "init")
83 }
84 // will throw if no permission
85 mMgr.addOnActiveSessionsChangedListener(mSessionsListener, null, mHandler)
86 mInit = true
87 postUpdateSessions()
88 mMgr.registerRemoteSessionCallback(mHandlerExecutor, mRemoteSessionCallback)
89 }
90
91 /** Destroy MediaSessions */
92 fun destroy() {
93 if (D.BUG) {
94 Log.d(TAG, "destroy")
95 }
96 mInit = false
97 mMgr.removeOnActiveSessionsChangedListener(mSessionsListener)
98 mMgr.unregisterRemoteSessionCallback(mRemoteSessionCallback)
99 }
100
101 /** Set volume `level` to remote media `token` */
102 fun setVolume(sessionId: SessionId, volumeLevel: Int) {
103 when (sessionId) {
104 is SessionId.Media -> setMediaSessionVolume(sessionId.token, volumeLevel)
105 }
106 }
107
108 private fun setMediaSessionVolume(token: MediaSession.Token, volumeLevel: Int) {
109 val record = mRecords[token]
110 if (record == null) {
111 Log.w(TAG, "setVolume: No record found for token $token")
112 return
113 }
114 if (D.BUG) {
115 Log.d(TAG, "Setting level to $volumeLevel")
116 }
117 record.controller.setVolumeTo(volumeLevel, 0)
118 }
119
120 private fun onRemoteVolumeChangedH(sessionToken: MediaSession.Token, flags: Int) {
121 val controller = MediaController(mContext, sessionToken)
122 if (D.BUG) {
123 Log.d(
124 TAG,
125 "remoteVolumeChangedH " +
126 controller.packageName +
127 " " +
128 Util.audioManagerFlagsToString(flags),
129 )
130 }
131 val token = controller.sessionToken
132 mCallbacks.onRemoteVolumeChanged(SessionId.from(token), flags)
133 }
134
135 private fun onUpdateRemoteSessionListH(sessionToken: MediaSession.Token?) {
136 if (D.BUG) {
137 Log.d(
138 TAG,
139 "onUpdateRemoteSessionListH ${sessionToken?.let {MediaController(mContext, it)}?.packageName}",
140 )
141 }
142 // this may be our only indication that a remote session is changed, refresh
143 postUpdateSessions()
144 }
145
146 private fun postUpdateSessions() {
147 if (mInit) {
148 mHandler.sendEmptyMessage(UPDATE_SESSIONS)
149 }
150 }
151
152 private fun onActiveSessionsUpdatedH(controllers: List<MediaController>) {
153 if (D.BUG) {
154 Log.d(TAG, "onActiveSessionsUpdatedH n=" + controllers.size)
155 }
156 val toRemove: MutableSet<MediaSession.Token> = HashSet(mRecords.keys)
157 for (controller in controllers) {
158 val token = controller.sessionToken
159 val playbackInfo = controller.playbackInfo
160 toRemove.remove(token)
161 if (!mRecords.containsKey(token)) {
162 val record = MediaControllerRecord(controller)
163 record.name = getControllerName(controller)
164 mRecords[token] = record
165 controller.registerCallback(record, mHandler)
166 }
167 val record = mRecords[token]
168 val remote = playbackInfo.isRemote()
169 if (remote) {
170 updateRemoteH(token, record!!.name, playbackInfo)
171 record.sentRemote = true
172 }
173 }
174 for (token in toRemove) {
175 val record = mRecords[token]!!
176 record.controller.unregisterCallback(record)
177 mRecords.remove(token)
178 if (D.BUG) {
179 Log.d(TAG, "Removing " + record.name + " sentRemote=" + record.sentRemote)
180 }
181 if (record.sentRemote) {
182 mCallbacks.onRemoteRemoved(SessionId.from(token))
183 record.sentRemote = false
184 }
185 }
186 }
187
188 private fun getControllerName(controller: MediaController): String {
189 val pm = mContext.packageManager
190 val pkg = controller.packageName
191 try {
192 if (USE_SERVICE_LABEL) {
193 val services =
194 pm.queryIntentServices(
195 Intent("android.media.MediaRouteProviderService").setPackage(pkg),
196 0,
197 )
198 if (services != null) {
199 for (ri in services) {
200 if (ri.serviceInfo == null) continue
201 if (pkg == ri.serviceInfo.packageName) {
202 val serviceLabel =
203 Objects.toString(ri.serviceInfo.loadLabel(pm), "").trim()
204 if (serviceLabel.isNotEmpty()) {
205 return serviceLabel
206 }
207 }
208 }
209 }
210 }
211 val ai = pm.getApplicationInfo(pkg, 0)
212 val appLabel = Objects.toString(ai.loadLabel(pm), "").trim { it <= ' ' }
213 if (appLabel.isNotEmpty()) {
214 return appLabel
215 }
216 } catch (_: PackageManager.NameNotFoundException) {}
217 return pkg
218 }
219
220 private fun updateRemoteH(
221 token: MediaSession.Token,
222 name: String?,
223 playbackInfo: PlaybackInfo,
224 ) = mCallbacks.onRemoteUpdate(SessionId.from(token), name, VolumeInfo.from(playbackInfo))
225
226 private inner class MediaControllerRecord(val controller: MediaController) :
227 MediaController.Callback() {
228 var sentRemote: Boolean = false
229 var name: String? = null
230
231 fun cb(method: String): String {
232 return method + " " + controller.packageName + " "
233 }
234
235 override fun onAudioInfoChanged(info: PlaybackInfo) {
236 if (D.BUG) {
237 Log.d(
238 TAG,
239 (cb("onAudioInfoChanged") +
240 Util.playbackInfoToString(info) +
241 " sentRemote=" +
242 sentRemote),
243 )
244 }
245 val remote = info.isRemote()
246 if (!remote && sentRemote) {
247 mCallbacks.onRemoteRemoved(SessionId.from(controller.sessionToken))
248 sentRemote = false
249 } else if (remote) {
250 updateRemoteH(controller.sessionToken, name, info)
251 sentRemote = true
252 }
253 }
254
255 override fun onExtrasChanged(extras: Bundle?) {
256 if (D.BUG) {
257 Log.d(TAG, cb("onExtrasChanged") + extras)
258 }
259 }
260
261 override fun onMetadataChanged(metadata: MediaMetadata?) {
262 if (D.BUG) {
263 Log.d(TAG, cb("onMetadataChanged") + Util.mediaMetadataToString(metadata))
264 }
265 }
266
267 override fun onPlaybackStateChanged(state: PlaybackState?) {
268 if (D.BUG) {
269 Log.d(TAG, cb("onPlaybackStateChanged") + Util.playbackStateToString(state))
270 }
271 }
272
273 override fun onQueueChanged(queue: List<MediaSession.QueueItem>?) {
274 if (D.BUG) {
275 Log.d(TAG, cb("onQueueChanged") + queue)
276 }
277 }
278
279 override fun onQueueTitleChanged(title: CharSequence?) {
280 if (D.BUG) {
281 Log.d(TAG, cb("onQueueTitleChanged") + title)
282 }
283 }
284
285 override fun onSessionDestroyed() {
286 if (D.BUG) {
287 Log.d(TAG, cb("onSessionDestroyed"))
288 }
289 }
290
291 override fun onSessionEvent(event: String, extras: Bundle?) {
292 if (D.BUG) {
293 Log.d(TAG, cb("onSessionEvent") + "event=" + event + " extras=" + extras)
294 }
295 }
296 }
297
298 private inner class H(looper: Looper) : Handler(looper) {
299
300 override fun handleMessage(msg: Message) {
301 when (msg.what) {
302 UPDATE_SESSIONS -> onActiveSessionsUpdatedH(mMgr.getActiveSessions(null))
303 REMOTE_VOLUME_CHANGED ->
304 onRemoteVolumeChangedH(msg.obj as MediaSession.Token, msg.arg1)
305 UPDATE_REMOTE_SESSION_LIST ->
306 onUpdateRemoteSessionListH(msg.obj as MediaSession.Token?)
307 }
308 }
309 }
310
311 /** Opaque id for ongoing sessions that support volume adjustment. */
312 sealed interface SessionId {
313
314 companion object {
315 fun from(token: MediaSession.Token) = Media(token)
316 }
317
318 data class Media(val token: MediaSession.Token) : SessionId
319 }
320
321 /** Holds session volume information. */
322 data class VolumeInfo(val currentVolume: Int, val maxVolume: Int) {
323
324 companion object {
325
326 fun from(playbackInfo: PlaybackInfo) =
327 VolumeInfo(playbackInfo.currentVolume, playbackInfo.maxVolume)
328 }
329 }
330
331 /** Callback for remote media sessions */
332 interface Callbacks {
333 /** Invoked when remote media session is updated */
334 fun onRemoteUpdate(token: SessionId?, name: String?, volumeInfo: VolumeInfo?)
335
336 /** Invoked when remote media session is removed */
337 fun onRemoteRemoved(token: SessionId?)
338
339 /** Invoked when remote volume is changed */
340 fun onRemoteVolumeChanged(token: SessionId?, flags: Int)
341 }
342
343 companion object {
344 private val TAG: String = Util.logTag(MediaSessions::class.java)
345
346 const val UPDATE_SESSIONS: Int = 1
347 const val REMOTE_VOLUME_CHANGED: Int = 2
348 const val UPDATE_REMOTE_SESSION_LIST: Int = 3
349
350 private const val USE_SERVICE_LABEL = false
351 }
352 }
353
isRemotenull354 private fun PlaybackInfo?.isRemote() = this?.playbackType == PlaybackInfo.PLAYBACK_TYPE_REMOTE
355
356 private fun MediaController.dump(n: Int, writer: PrintWriter) {
357 writer.println(" Controller $n: $packageName")
358
359 writer.println(" PlaybackState: ${Util.playbackStateToString(playbackState)}")
360 writer.println(" PlaybackInfo: ${Util.playbackInfoToString(playbackInfo)}")
361 val metadata = this.metadata
362 if (metadata != null) {
363 writer.println(" MediaMetadata.desc=${metadata.description}")
364 }
365 writer.println(" RatingType: $ratingType")
366 writer.println(" Flags: $flags")
367
368 writer.println(" Extras:")
369 val extras = this.extras
370 if (extras == null) {
371 writer.println(" <null>")
372 } else {
373 for (key in extras.keySet()) {
374 writer.println(" $key=${extras[key]}")
375 }
376 }
377 writer.println(" QueueTitle: $queueTitle")
378 val queue = this.queue
379 if (!queue.isNullOrEmpty()) {
380 writer.println(" Queue:")
381 for (qi in queue) {
382 writer.println(" $qi")
383 }
384 }
385 writer.println(" sessionActivity: $sessionActivity")
386 }
387