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