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 }