Home | History | Annotate | Download | only in phone
      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          */
     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 
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    268     private void notifyActionsChanged() {
    269         if (!mListeners.isEmpty()) {
    270             List<RemoteAction> actions = getMediaActions();
    271             mListeners.forEach(l -> l.onMediaActionsChanged(actions));
    272         }
    273     }
    274 }
    275