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