Home | History | Annotate | Download | only in player
      1 /*
      2  * Copyright (C) 2013 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.example.android.mediarouter.player;
     18 
     19 import android.app.PendingIntent;
     20 import android.net.Uri;
     21 import android.support.v7.media.MediaItemStatus;
     22 import android.support.v7.media.MediaSessionStatus;
     23 import android.util.Log;
     24 
     25 import com.example.android.mediarouter.player.Player.Callback;
     26 
     27 import java.util.ArrayList;
     28 import java.util.List;
     29 
     30 /**
     31  * SessionManager manages a media session as a queue. It supports common
     32  * queuing behaviors such as enqueue/remove of media items, pause/resume/stop,
     33  * etc.
     34  *
     35  * Actual playback of a single media item is abstracted into a Player interface,
     36  * and is handled outside this class.
     37  */
     38 public class SessionManager implements Callback {
     39     private static final String TAG = "SessionManager";
     40     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
     41 
     42     private String mName;
     43     private int mSessionId;
     44     private int mItemId;
     45     private boolean mPaused;
     46     private boolean mSessionValid;
     47     private Player mPlayer;
     48     private Callback mCallback;
     49     private List<PlaylistItem> mPlaylist = new ArrayList<PlaylistItem>();
     50 
     51     public SessionManager(String name) {
     52         mName = name;
     53     }
     54 
     55     public boolean hasSession() {
     56         return mSessionValid;
     57     }
     58 
     59     public String getSessionId() {
     60         return mSessionValid ? Integer.toString(mSessionId) : null;
     61     }
     62 
     63     public PlaylistItem getCurrentItem() {
     64         return mPlaylist.isEmpty() ? null : mPlaylist.get(0);
     65     }
     66 
     67     // Get the cached statistic info from the player (will not update it)
     68     public String getStatistics() {
     69         checkPlayer();
     70         return mPlayer.getStatistics();
     71     }
     72 
     73     // Returns the cached playlist (note this is not responsible for updating it)
     74     public List<PlaylistItem> getPlaylist() {
     75         return mPlaylist;
     76     }
     77 
     78     // Updates the playlist asynchronously, calls onPlaylistReady() when finished.
     79     public void updateStatus() {
     80         if (DEBUG) {
     81             log("updateStatus");
     82         }
     83         checkPlayer();
     84         // update the statistics first, so that the stats string is valid when
     85         // onPlaylistReady() gets called in the end
     86         mPlayer.updateStatistics();
     87 
     88         if (mPlaylist.isEmpty()) {
     89             // If queue is empty, don't forget to call onPlaylistReady()!
     90             onPlaylistReady();
     91         } else if (mPlayer.isQueuingSupported()) {
     92             // If player supports queuing, get status of each item. Player is
     93             // responsible to call onPlaylistReady() after last getStatus().
     94             // (update=1 requires player to callback onPlaylistReady())
     95             for (int i = 0; i < mPlaylist.size(); i++) {
     96                 PlaylistItem item = mPlaylist.get(i);
     97                 mPlayer.getStatus(item, (i == mPlaylist.size() - 1) /* update */);
     98             }
     99         } else {
    100             // Otherwise, only need to get status for current item. Player is
    101             // responsible to call onPlaylistReady() when finished.
    102             mPlayer.getStatus(getCurrentItem(), true /* update */);
    103         }
    104     }
    105 
    106     public PlaylistItem add(Uri uri, String mime) {
    107         return add(uri, mime, null);
    108     }
    109 
    110     public PlaylistItem add(Uri uri, String mime, PendingIntent receiver) {
    111         if (DEBUG) {
    112             log("add: uri=" + uri + ", receiver=" + receiver);
    113         }
    114         // create new session if needed
    115         startSession();
    116         checkPlayerAndSession();
    117 
    118         // append new item with initial status PLAYBACK_STATE_PENDING
    119         PlaylistItem item = new PlaylistItem(
    120                 Integer.toString(mSessionId), Integer.toString(mItemId), uri, mime, receiver);
    121         mPlaylist.add(item);
    122         mItemId++;
    123 
    124         // if player supports queuing, enqueue the item now
    125         if (mPlayer.isQueuingSupported()) {
    126             mPlayer.enqueue(item);
    127         }
    128         updatePlaybackState();
    129         return item;
    130     }
    131 
    132     public PlaylistItem remove(String iid) {
    133         if (DEBUG) {
    134             log("remove: iid=" + iid);
    135         }
    136         checkPlayerAndSession();
    137         return removeItem(iid, MediaItemStatus.PLAYBACK_STATE_CANCELED);
    138     }
    139 
    140     public PlaylistItem seek(String iid, long pos) {
    141         if (DEBUG) {
    142             log("seek: iid=" + iid +", pos=" + pos);
    143         }
    144         checkPlayerAndSession();
    145         // seeking on pending items are not yet supported
    146         checkItemCurrent(iid);
    147 
    148         PlaylistItem item = getCurrentItem();
    149         if (pos != item.getPosition()) {
    150             item.setPosition(pos);
    151             if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING
    152                     || item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) {
    153                 mPlayer.seek(item);
    154             }
    155         }
    156         return item;
    157     }
    158 
    159     public PlaylistItem getStatus(String iid) {
    160         checkPlayerAndSession();
    161 
    162         // This should only be called for local player. Remote player is
    163         // asynchronous, need to use updateStatus() instead.
    164         if (mPlayer.isRemotePlayback()) {
    165             throw new IllegalStateException(
    166                     "getStatus should not be called on remote player!");
    167         }
    168 
    169         for (PlaylistItem item : mPlaylist) {
    170             if (item.getItemId().equals(iid)) {
    171                 if (item == getCurrentItem()) {
    172                     mPlayer.getStatus(item, false);
    173                 }
    174                 return item;
    175             }
    176         }
    177         return null;
    178     }
    179 
    180     public void pause() {
    181         if (DEBUG) {
    182             log("pause");
    183         }
    184         mPaused = true;
    185         updatePlaybackState();
    186     }
    187 
    188     public void resume() {
    189         if (DEBUG) {
    190             log("resume");
    191         }
    192         mPaused = false;
    193         updatePlaybackState();
    194     }
    195 
    196     public void stop() {
    197         if (DEBUG) {
    198             log("stop");
    199         }
    200         mPlayer.stop();
    201         mPlaylist.clear();
    202         mPaused = false;
    203         updateStatus();
    204     }
    205 
    206     public String startSession() {
    207         if (!mSessionValid) {
    208             mSessionId++;
    209             mItemId = 0;
    210             mPaused = false;
    211             mSessionValid = true;
    212             return Integer.toString(mSessionId);
    213         }
    214         return null;
    215     }
    216 
    217     public boolean endSession() {
    218         if (mSessionValid) {
    219             mSessionValid = false;
    220             return true;
    221         }
    222         return false;
    223     }
    224 
    225     public MediaSessionStatus getSessionStatus(String sid) {
    226         int sessionState = (sid != null && sid.equals(mSessionId)) ?
    227                 MediaSessionStatus.SESSION_STATE_ACTIVE :
    228                     MediaSessionStatus.SESSION_STATE_INVALIDATED;
    229 
    230         return new MediaSessionStatus.Builder(sessionState)
    231                 .setQueuePaused(mPaused)
    232                 .build();
    233     }
    234 
    235     // Suspend the playback manager. Put the current item back into PENDING
    236     // state, and remember the current playback position. Called when switching
    237     // to a different player (route).
    238     public void suspend(long pos) {
    239         for (PlaylistItem item : mPlaylist) {
    240             item.setRemoteItemId(null);
    241             item.setDuration(0);
    242         }
    243         PlaylistItem item = getCurrentItem();
    244         if (DEBUG) {
    245             log("suspend: item=" + item + ", pos=" + pos);
    246         }
    247         if (item != null) {
    248             if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING
    249                     || item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) {
    250                 item.setState(MediaItemStatus.PLAYBACK_STATE_PENDING);
    251                 item.setPosition(pos);
    252             }
    253         }
    254     }
    255 
    256     // Unsuspend the playback manager. Restart playback on new player (route).
    257     // This will resume playback of current item. Furthermore, if the new player
    258     // supports queuing, playlist will be re-established on the remote player.
    259     public void unsuspend() {
    260         if (DEBUG) {
    261             log("unsuspend");
    262         }
    263         if (mPlayer.isQueuingSupported()) {
    264             for (PlaylistItem item : mPlaylist) {
    265                 mPlayer.enqueue(item);
    266             }
    267         }
    268         updatePlaybackState();
    269     }
    270 
    271     // Player.Callback
    272     @Override
    273     public void onError() {
    274         finishItem(true);
    275     }
    276 
    277     @Override
    278     public void onCompletion() {
    279         finishItem(false);
    280     }
    281 
    282     @Override
    283     public void onPlaylistChanged() {
    284         // Playlist has changed, update the cached playlist
    285         updateStatus();
    286     }
    287 
    288     @Override
    289     public void onPlaylistReady() {
    290         // Notify activity to update Ui
    291         if (mCallback != null) {
    292             mCallback.onStatusChanged();
    293         }
    294     }
    295 
    296     private void log(String message) {
    297         Log.d(TAG, mName + ": " + message);
    298     }
    299 
    300     private void checkPlayer() {
    301         if (mPlayer == null) {
    302             throw new IllegalStateException("Player not set!");
    303         }
    304     }
    305 
    306     private void checkSession() {
    307         if (!mSessionValid) {
    308             throw new IllegalStateException("Session not set!");
    309         }
    310     }
    311 
    312     private void checkPlayerAndSession() {
    313         checkPlayer();
    314         checkSession();
    315     }
    316 
    317     private void checkItemCurrent(String iid) {
    318         PlaylistItem item = getCurrentItem();
    319         if (item == null || !item.getItemId().equals(iid)) {
    320             throw new IllegalArgumentException("Item is not current!");
    321         }
    322     }
    323 
    324     private void updatePlaybackState() {
    325         PlaylistItem item = getCurrentItem();
    326         if (item != null) {
    327             if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PENDING) {
    328                 item.setState(mPaused ? MediaItemStatus.PLAYBACK_STATE_PAUSED
    329                         : MediaItemStatus.PLAYBACK_STATE_PLAYING);
    330                 if (!mPlayer.isQueuingSupported()) {
    331                     mPlayer.play(item);
    332                 }
    333             } else if (mPaused && item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING) {
    334                 mPlayer.pause();
    335                 item.setState(MediaItemStatus.PLAYBACK_STATE_PAUSED);
    336             } else if (!mPaused && item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) {
    337                 mPlayer.resume();
    338                 item.setState(MediaItemStatus.PLAYBACK_STATE_PLAYING);
    339             }
    340             // notify client that item playback status has changed
    341             if (mCallback != null) {
    342                 mCallback.onItemChanged(item);
    343             }
    344         }
    345         updateStatus();
    346     }
    347 
    348     private PlaylistItem removeItem(String iid, int state) {
    349         checkPlayerAndSession();
    350         List<PlaylistItem> queue =
    351                 new ArrayList<PlaylistItem>(mPlaylist.size());
    352         PlaylistItem found = null;
    353         for (PlaylistItem item : mPlaylist) {
    354             if (iid.equals(item.getItemId())) {
    355                 if (mPlayer.isQueuingSupported()) {
    356                     mPlayer.remove(item.getRemoteItemId());
    357                 } else if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING
    358                         || item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED){
    359                     mPlayer.stop();
    360                 }
    361                 item.setState(state);
    362                 found = item;
    363                 // notify client that item is now removed
    364                 if (mCallback != null) {
    365                     mCallback.onItemChanged(found);
    366                 }
    367             } else {
    368                 queue.add(item);
    369             }
    370         }
    371         if (found != null) {
    372             mPlaylist = queue;
    373             updatePlaybackState();
    374         } else {
    375             log("item not found");
    376         }
    377         return found;
    378     }
    379 
    380     private void finishItem(boolean error) {
    381         PlaylistItem item = getCurrentItem();
    382         if (item != null) {
    383             removeItem(item.getItemId(), error ?
    384                     MediaItemStatus.PLAYBACK_STATE_ERROR :
    385                         MediaItemStatus.PLAYBACK_STATE_FINISHED);
    386             updateStatus();
    387         }
    388     }
    389 
    390     // set the Player that this playback manager will interact with
    391     public void setPlayer(Player player) {
    392         mPlayer = player;
    393         checkPlayer();
    394         mPlayer.setCallback(this);
    395     }
    396 
    397     // provide a callback interface to tell the UI when significant state changes occur
    398     public void setCallback(Callback callback) {
    399         mCallback = callback;
    400     }
    401 
    402     @Override
    403     public String toString() {
    404         String result = "Media Queue: ";
    405         if (!mPlaylist.isEmpty()) {
    406             for (PlaylistItem item : mPlaylist) {
    407                 result += "\n" + item.toString();
    408             }
    409         } else {
    410             result += "<empty>";
    411         }
    412         return result;
    413     }
    414 
    415     public interface Callback {
    416         void onStatusChanged();
    417         void onItemChanged(PlaylistItem item);
    418     }
    419 }
    420