1 /* 2 * Copyright (C) 2017 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.systemui.pip.phone; 18 19 import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; 20 21 import android.app.IActivityManager; 22 import android.app.PendingIntent; 23 import android.app.RemoteAction; 24 import android.content.BroadcastReceiver; 25 import android.content.ComponentName; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.content.IntentFilter; 29 import android.graphics.drawable.Drawable; 30 import android.graphics.drawable.Icon; 31 import android.media.session.MediaController; 32 import android.media.session.MediaSession; 33 import android.media.session.MediaSessionManager; 34 import android.media.session.MediaSessionManager.OnActiveSessionsChangedListener; 35 import android.media.session.PlaybackState; 36 import android.os.UserHandle; 37 38 import com.android.systemui.Dependency; 39 import com.android.systemui.R; 40 import com.android.systemui.statusbar.policy.UserInfoController; 41 42 import java.util.ArrayList; 43 import java.util.Collections; 44 import java.util.List; 45 46 /** 47 * Interfaces with the {@link MediaSessionManager} to compose the right set of actions to show (only 48 * if there are no actions from the PiP activity itself). The active media controller is only set 49 * when there is a media session from the top PiP activity. 50 */ 51 public class PipMediaController { 52 53 private static final String ACTION_PLAY = "com.android.systemui.pip.phone.PLAY"; 54 private static final String ACTION_PAUSE = "com.android.systemui.pip.phone.PAUSE"; 55 private static final String ACTION_NEXT = "com.android.systemui.pip.phone.NEXT"; 56 private static final String ACTION_PREV = "com.android.systemui.pip.phone.PREV"; 57 58 /** 59 * A listener interface to receive notification on changes to the media actions. 60 */ 61 public interface ActionListener { 62 /** 63 * Called when the media actions changes. 64 */ onMediaActionsChanged(List<RemoteAction> actions)65 void onMediaActionsChanged(List<RemoteAction> actions); 66 } 67 68 private final Context mContext; 69 private final IActivityManager mActivityManager; 70 71 private final MediaSessionManager mMediaSessionManager; 72 private MediaController mMediaController; 73 74 private RemoteAction mPauseAction; 75 private RemoteAction mPlayAction; 76 private RemoteAction mNextAction; 77 private RemoteAction mPrevAction; 78 79 private BroadcastReceiver mPlayPauseActionReceiver = new BroadcastReceiver() { 80 @Override 81 public void onReceive(Context context, Intent intent) { 82 final String action = intent.getAction(); 83 if (action.equals(ACTION_PLAY)) { 84 mMediaController.getTransportControls().play(); 85 } else if (action.equals(ACTION_PAUSE)) { 86 mMediaController.getTransportControls().pause(); 87 } else if (action.equals(ACTION_NEXT)) { 88 mMediaController.getTransportControls().skipToNext(); 89 } else if (action.equals(ACTION_PREV)) { 90 mMediaController.getTransportControls().skipToPrevious(); 91 } 92 } 93 }; 94 95 private final MediaController.Callback mPlaybackChangedListener = new MediaController.Callback() { 96 @Override 97 public void onPlaybackStateChanged(PlaybackState state) { 98 notifyActionsChanged(); 99 } 100 }; 101 102 private final MediaSessionManager.OnActiveSessionsChangedListener mSessionsChangedListener = 103 new OnActiveSessionsChangedListener() { 104 @Override 105 public void onActiveSessionsChanged(List<MediaController> controllers) { 106 resolveActiveMediaController(controllers); 107 } 108 }; 109 110 private ArrayList<ActionListener> mListeners = new ArrayList<>(); 111 PipMediaController(Context context, IActivityManager activityManager)112 public PipMediaController(Context context, IActivityManager activityManager) { 113 mContext = context; 114 mActivityManager = activityManager; 115 IntentFilter mediaControlFilter = new IntentFilter(); 116 mediaControlFilter.addAction(ACTION_PLAY); 117 mediaControlFilter.addAction(ACTION_PAUSE); 118 mediaControlFilter.addAction(ACTION_NEXT); 119 mediaControlFilter.addAction(ACTION_PREV); 120 mContext.registerReceiver(mPlayPauseActionReceiver, mediaControlFilter); 121 122 createMediaActions(); 123 mMediaSessionManager = 124 (MediaSessionManager) context.getSystemService(Context.MEDIA_SESSION_SERVICE); 125 126 // The media session listener needs to be re-registered when switching users 127 UserInfoController userInfoController = Dependency.get(UserInfoController.class); 128 userInfoController.addCallback((String name, Drawable picture, String userAccount) -> 129 registerSessionListenerForCurrentUser()); 130 } 131 132 /** 133 * Handles when an activity is pinned. 134 */ onActivityPinned()135 public void onActivityPinned() { 136 // Once we enter PiP, try to find the active media controller for the top most activity 137 resolveActiveMediaController(mMediaSessionManager.getActiveSessionsForUser(null, 138 UserHandle.USER_CURRENT)); 139 } 140 141 /** 142 * Adds a new media action listener. 143 */ addListener(ActionListener listener)144 public void addListener(ActionListener listener) { 145 if (!mListeners.contains(listener)) { 146 mListeners.add(listener); 147 listener.onMediaActionsChanged(getMediaActions()); 148 } 149 } 150 151 /** 152 * Removes a media action listener. 153 */ removeListener(ActionListener listener)154 public void removeListener(ActionListener listener) { 155 listener.onMediaActionsChanged(Collections.EMPTY_LIST); 156 mListeners.remove(listener); 157 } 158 159 /** 160 * Gets the set of media actions currently available. 161 */ getMediaActions()162 private List<RemoteAction> getMediaActions() { 163 if (mMediaController == null || mMediaController.getPlaybackState() == null) { 164 return Collections.EMPTY_LIST; 165 } 166 167 ArrayList<RemoteAction> mediaActions = new ArrayList<>(); 168 int state = mMediaController.getPlaybackState().getState(); 169 boolean isPlaying = MediaSession.isActiveState(state); 170 long actions = mMediaController.getPlaybackState().getActions(); 171 172 // Prev action 173 mPrevAction.setEnabled((actions & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0); 174 mediaActions.add(mPrevAction); 175 176 // Play/pause action 177 if (!isPlaying && ((actions & PlaybackState.ACTION_PLAY) != 0)) { 178 mediaActions.add(mPlayAction); 179 } else if (isPlaying && ((actions & PlaybackState.ACTION_PAUSE) != 0)) { 180 mediaActions.add(mPauseAction); 181 } 182 183 // Next action 184 mNextAction.setEnabled((actions & PlaybackState.ACTION_SKIP_TO_NEXT) != 0); 185 mediaActions.add(mNextAction); 186 return mediaActions; 187 } 188 189 /** 190 * Creates the standard media buttons that we may show. 191 */ createMediaActions()192 private void createMediaActions() { 193 String pauseDescription = mContext.getString(R.string.pip_pause); 194 mPauseAction = new RemoteAction(Icon.createWithResource(mContext, 195 R.drawable.ic_pause_white), pauseDescription, pauseDescription, 196 PendingIntent.getBroadcast(mContext, 0, new Intent(ACTION_PAUSE), 197 FLAG_UPDATE_CURRENT)); 198 199 String playDescription = mContext.getString(R.string.pip_play); 200 mPlayAction = new RemoteAction(Icon.createWithResource(mContext, 201 R.drawable.ic_play_arrow_white), playDescription, playDescription, 202 PendingIntent.getBroadcast(mContext, 0, new Intent(ACTION_PLAY), 203 FLAG_UPDATE_CURRENT)); 204 205 String nextDescription = mContext.getString(R.string.pip_skip_to_next); 206 mNextAction = new RemoteAction(Icon.createWithResource(mContext, 207 R.drawable.ic_skip_next_white), nextDescription, nextDescription, 208 PendingIntent.getBroadcast(mContext, 0, new Intent(ACTION_NEXT), 209 FLAG_UPDATE_CURRENT)); 210 211 String prevDescription = mContext.getString(R.string.pip_skip_to_prev); 212 mPrevAction = new RemoteAction(Icon.createWithResource(mContext, 213 R.drawable.ic_skip_previous_white), prevDescription, prevDescription, 214 PendingIntent.getBroadcast(mContext, 0, new Intent(ACTION_PREV), 215 FLAG_UPDATE_CURRENT)); 216 } 217 218 /** 219 * Re-registers the session listener for the current user. 220 */ registerSessionListenerForCurrentUser()221 private void registerSessionListenerForCurrentUser() { 222 mMediaSessionManager.removeOnActiveSessionsChangedListener(mSessionsChangedListener); 223 mMediaSessionManager.addOnActiveSessionsChangedListener(mSessionsChangedListener, null, 224 UserHandle.USER_CURRENT, null); 225 } 226 227 /** 228 * Tries to find and set the active media controller for the top PiP activity. 229 */ resolveActiveMediaController(List<MediaController> controllers)230 private void resolveActiveMediaController(List<MediaController> controllers) { 231 if (controllers != null) { 232 final ComponentName topActivity = PipUtils.getTopPinnedActivity(mContext, 233 mActivityManager).first; 234 if (topActivity != null) { 235 for (int i = 0; i < controllers.size(); i++) { 236 final MediaController controller = controllers.get(i); 237 if (controller.getPackageName().equals(topActivity.getPackageName())) { 238 setActiveMediaController(controller); 239 return; 240 } 241 } 242 } 243 } 244 setActiveMediaController(null); 245 } 246 247 /** 248 * Sets the active media controller for the top PiP activity. 249 */ setActiveMediaController(MediaController controller)250 private void setActiveMediaController(MediaController controller) { 251 if (controller != mMediaController) { 252 if (mMediaController != null) { 253 mMediaController.unregisterCallback(mPlaybackChangedListener); 254 } 255 mMediaController = controller; 256 if (controller != null) { 257 controller.registerCallback(mPlaybackChangedListener); 258 } 259 notifyActionsChanged(); 260 261 // TODO(winsonc): Consider if we want to close the PIP after a timeout (like on TV) 262 } 263 } 264 265 /** 266 * Notifies all listeners that the actions have changed. 267 */ notifyActionsChanged()268 private void notifyActionsChanged() { 269 if (!mListeners.isEmpty()) { 270 List<RemoteAction> actions = getMediaActions(); 271 mListeners.forEach(l -> l.onMediaActionsChanged(actions)); 272 } 273 } 274 } 275