• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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