1 /*
<lambda>null2  * Copyright (C) 2023 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.wm.shell.common.pip
17 
18 import android.annotation.DrawableRes
19 import android.annotation.StringRes
20 import android.app.PendingIntent
21 import android.app.RemoteAction
22 import android.content.BroadcastReceiver
23 import android.content.Context
24 import android.content.Intent
25 import android.content.IntentFilter
26 import android.graphics.drawable.Icon
27 import android.media.MediaMetadata
28 import android.media.session.MediaController
29 import android.media.session.MediaSession
30 import android.media.session.MediaSessionManager
31 import android.media.session.PlaybackState
32 import android.os.Handler
33 import android.os.HandlerExecutor
34 import android.os.UserHandle
35 import com.android.wm.shell.R
36 import java.util.function.Consumer
37 
38 /**
39  * Interfaces with the [MediaSessionManager] to compose the right set of actions to show (only
40  * if there are no actions from the PiP activity itself). The active media controller is only set
41  * when there is a media session from the top PiP activity.
42  */
43 class PipMediaController(private val mContext: Context, private val mMainHandler: Handler) {
44     /**
45      * A listener interface to receive notification on changes to the media actions.
46      */
47     interface ActionListener {
48         /**
49          * Called when the media actions changed.
50          */
51         fun onMediaActionsChanged(actions: List<RemoteAction?>?)
52     }
53 
54     /**
55      * A listener interface to receive notification on changes to the media metadata.
56      */
57     interface MetadataListener {
58         /**
59          * Called when the media metadata changed.
60          */
61         fun onMediaMetadataChanged(metadata: MediaMetadata?)
62     }
63 
64     /**
65      * A listener interface to receive notification on changes to the media session token.
66      */
67     interface TokenListener {
68         /**
69          * Called when the media session token changed.
70          */
71         fun onMediaSessionTokenChanged(token: MediaSession.Token?)
72     }
73 
74     private val mHandlerExecutor: HandlerExecutor = HandlerExecutor(mMainHandler)
75     private val mMediaSessionManager: MediaSessionManager?
76     private var mMediaController: MediaController? = null
77     private val mPauseAction: RemoteAction
78     private val mPlayAction: RemoteAction
79     private val mNextAction: RemoteAction
80     private val mPrevAction: RemoteAction
81     private val mMediaActionReceiver: BroadcastReceiver = object : BroadcastReceiver() {
82         override fun onReceive(context: Context, intent: Intent) {
83             if (mMediaController == null) {
84                 // no active media session, bail early.
85                 return
86             }
87             when (intent.action) {
88                 ACTION_PLAY -> mMediaController!!.transportControls.play()
89                 ACTION_PAUSE -> mMediaController!!.transportControls.pause()
90                 ACTION_NEXT -> mMediaController!!.transportControls.skipToNext()
91                 ACTION_PREV -> mMediaController!!.transportControls.skipToPrevious()
92             }
93         }
94     }
95     private val mPlaybackChangedListener: MediaController.Callback =
96         object : MediaController.Callback() {
97             override fun onPlaybackStateChanged(state: PlaybackState?) {
98                 notifyActionsChanged()
99             }
100 
101             override fun onMetadataChanged(metadata: MediaMetadata?) {
102                 notifyMetadataChanged(metadata)
103             }
104         }
105     private val mSessionsChangedListener =
106         MediaSessionManager.OnActiveSessionsChangedListener { controllers: List<MediaController>? ->
107             resolveActiveMediaController(controllers)
108         }
109     private val mActionListeners = ArrayList<ActionListener>()
110     private val mMetadataListeners = ArrayList<MetadataListener>()
111     private val mTokenListeners = ArrayList<TokenListener>()
112 
113     init {
114         val mediaControlFilter = IntentFilter()
115         mediaControlFilter.addAction(ACTION_PLAY)
116         mediaControlFilter.addAction(ACTION_PAUSE)
117         mediaControlFilter.addAction(ACTION_NEXT)
118         mediaControlFilter.addAction(ACTION_PREV)
119         mContext.registerReceiverForAllUsers(
120             mMediaActionReceiver, mediaControlFilter,
121             SYSTEMUI_PERMISSION, mMainHandler, Context.RECEIVER_EXPORTED
122         )
123 
124         // Creates the standard media buttons that we may show.
125         mPauseAction = getDefaultRemoteAction(
126             R.string.pip_pause,
127             R.drawable.pip_ic_pause_white, ACTION_PAUSE
128         )
129         mPlayAction = getDefaultRemoteAction(
130             R.string.pip_play,
131             R.drawable.pip_ic_play_arrow_white, ACTION_PLAY
132         )
133         mNextAction = getDefaultRemoteAction(
134             R.string.pip_skip_to_next,
135             R.drawable.pip_ic_skip_next_white, ACTION_NEXT
136         )
137         mPrevAction = getDefaultRemoteAction(
138             R.string.pip_skip_to_prev,
139             R.drawable.pip_ic_skip_previous_white, ACTION_PREV
140         )
141         mMediaSessionManager = mContext.getSystemService(
142             MediaSessionManager::class.java
143         )
144     }
145 
146     /**
147      * Handles when an activity is pinned.
148      */
149     fun onActivityPinned() {
150         // Once we enter PiP, try to find the active media controller for the top most activity
151         resolveActiveMediaController(
152             mMediaSessionManager!!.getActiveSessionsForUser(
153                 null,
154                 UserHandle.CURRENT
155             )
156         )
157     }
158 
159     /**
160      * Adds a new media action listener.
161      */
162     fun addActionListener(listener: ActionListener) {
163         if (!mActionListeners.contains(listener)) {
164             mActionListeners.add(listener)
165             listener.onMediaActionsChanged(mediaActions)
166         }
167     }
168 
169     /**
170      * Removes a media action listener.
171      */
172     fun removeActionListener(listener: ActionListener) {
173         listener.onMediaActionsChanged(emptyList<RemoteAction>())
174         mActionListeners.remove(listener)
175     }
176 
177     /**
178      * Adds a new media metadata listener.
179      */
180     fun addMetadataListener(listener: MetadataListener) {
181         if (!mMetadataListeners.contains(listener)) {
182             mMetadataListeners.add(listener)
183             listener.onMediaMetadataChanged(mediaMetadata)
184         }
185     }
186 
187     /**
188      * Removes a media metadata listener.
189      */
190     fun removeMetadataListener(listener: MetadataListener) {
191         listener.onMediaMetadataChanged(null)
192         mMetadataListeners.remove(listener)
193     }
194 
195     /**
196      * Adds a new token listener.
197      */
198     fun addTokenListener(listener: TokenListener) {
199         if (!mTokenListeners.contains(listener)) {
200             mTokenListeners.add(listener)
201             listener.onMediaSessionTokenChanged(token)
202         }
203     }
204 
205     /**
206      * Removes a token listener.
207      */
208     fun removeTokenListener(listener: TokenListener) {
209         listener.onMediaSessionTokenChanged(null)
210         mTokenListeners.remove(listener)
211     }
212 
213     private val token: MediaSession.Token?
214         get() = if (mMediaController == null) {
215             null
216         } else mMediaController!!.sessionToken
217     private val mediaMetadata: MediaMetadata?
218         get() = if (mMediaController != null) mMediaController!!.metadata else null
219 
220     private val mediaActions: List<RemoteAction?>
221         /**
222          * Gets the set of media actions currently available.
223          */
224         get() {
225             if (mMediaController == null) {
226                 return emptyList<RemoteAction>()
227             }
228             // Cache the PlaybackState since it's a Binder call.
229             // Safe because mMediaController is guaranteed non-null here.
230             val playbackState: PlaybackState = mMediaController!!.playbackState
231                 ?: return emptyList<RemoteAction>()
232             val mediaActions = ArrayList<RemoteAction?>()
233             val isPlaying = playbackState.isActive
234             val actions = playbackState.actions
235 
236             // Prev action
237             mPrevAction.isEnabled =
238                 actions and PlaybackState.ACTION_SKIP_TO_PREVIOUS != 0L
239             mediaActions.add(mPrevAction)
240 
241             // Play/pause action
242             if (!isPlaying && actions and PlaybackState.ACTION_PLAY != 0L) {
243                 mediaActions.add(mPlayAction)
244             } else if (isPlaying && actions and PlaybackState.ACTION_PAUSE != 0L) {
245                 mediaActions.add(mPauseAction)
246             }
247 
248             // Next action
249             mNextAction.isEnabled =
250                 actions and PlaybackState.ACTION_SKIP_TO_NEXT != 0L
251             mediaActions.add(mNextAction)
252             return mediaActions
253         }
254 
255     /** @return Default [RemoteAction] sends broadcast back to SysUI.
256      */
257     private fun getDefaultRemoteAction(
258         @StringRes titleAndDescription: Int,
259         @DrawableRes icon: Int,
260         action: String
261     ): RemoteAction {
262         val titleAndDescriptionStr = mContext.getString(titleAndDescription)
263         val intent = Intent(action)
264         intent.setPackage(mContext.packageName)
265         return RemoteAction(
266             Icon.createWithResource(mContext, icon),
267             titleAndDescriptionStr, titleAndDescriptionStr,
268             PendingIntent.getBroadcast(
269                 mContext, 0 /* requestCode */, intent,
270                 PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
271             )
272         )
273     }
274 
275     /**
276      * Re-registers the session listener for the current user.
277      */
278     fun registerSessionListenerForCurrentUser() {
279         mMediaSessionManager!!.removeOnActiveSessionsChangedListener(mSessionsChangedListener)
280         mMediaSessionManager.addOnActiveSessionsChangedListener(
281             null, UserHandle.CURRENT,
282             mHandlerExecutor, mSessionsChangedListener
283         )
284     }
285 
286     /**
287      * Tries to find and set the active media controller for the top PiP activity.
288      */
289     private fun resolveActiveMediaController(controllers: List<MediaController>?) {
290         if (controllers != null) {
291             val topActivity = PipUtils.getTopPipActivity(mContext).first
292             if (topActivity != null) {
293                 for (i in controllers.indices) {
294                     val controller = controllers[i]
295                     if (controller.packageName == topActivity.packageName) {
296                         setActiveMediaController(controller)
297                         return
298                     }
299                 }
300             }
301         }
302         setActiveMediaController(null)
303     }
304 
305     /**
306      * Sets the active media controller for the top PiP activity.
307      */
308     private fun setActiveMediaController(controller: MediaController?) {
309         if (controller != mMediaController) {
310             if (mMediaController != null) {
311                 mMediaController!!.unregisterCallback(mPlaybackChangedListener)
312             }
313             mMediaController = controller
314             controller?.registerCallback(mPlaybackChangedListener, mMainHandler)
315             notifyActionsChanged()
316             notifyMetadataChanged(mediaMetadata)
317             notifyTokenChanged(token)
318 
319             // TODO(winsonc): Consider if we want to close the PIP after a timeout (like on TV)
320         }
321     }
322 
323     /**
324      * Notifies all listeners that the actions have changed.
325      */
326     private fun notifyActionsChanged() {
327         if (mActionListeners.isNotEmpty()) {
328             val actions = mediaActions
329             mActionListeners.forEach(
330                 Consumer { l: ActionListener -> l.onMediaActionsChanged(actions) })
331         }
332     }
333 
334     /**
335      * Notifies all listeners that the metadata have changed.
336      */
337     private fun notifyMetadataChanged(metadata: MediaMetadata?) {
338         if (mMetadataListeners.isNotEmpty()) {
339             mMetadataListeners.forEach(Consumer { l: MetadataListener ->
340                 l.onMediaMetadataChanged(
341                     metadata
342                 )
343             })
344         }
345     }
346 
347     private fun notifyTokenChanged(token: MediaSession.Token?) {
348         if (mTokenListeners.isNotEmpty()) {
349             mTokenListeners.forEach(Consumer { l: TokenListener ->
350                 l.onMediaSessionTokenChanged(
351                     token
352                 )
353             })
354         }
355     }
356 
357     companion object {
358         private const val SYSTEMUI_PERMISSION = "com.android.systemui.permission.SELF"
359         private const val ACTION_PLAY = "com.android.wm.shell.pip.PLAY"
360         private const val ACTION_PAUSE = "com.android.wm.shell.pip.PAUSE"
361         private const val ACTION_NEXT = "com.android.wm.shell.pip.NEXT"
362         private const val ACTION_PREV = "com.android.wm.shell.pip.PREV"
363     }
364 }