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