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 
SessionManager(String name)51     public SessionManager(String name) {
52         mName = name;
53     }
54 
hasSession()55     public boolean hasSession() {
56         return mSessionValid;
57     }
58 
getSessionId()59     public String getSessionId() {
60         return mSessionValid ? Integer.toString(mSessionId) : null;
61     }
62 
getCurrentItem()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)
getStatistics()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)
getPlaylist()74     public List<PlaylistItem> getPlaylist() {
75         return mPlaylist;
76     }
77 
78     // Updates the playlist asynchronously, calls onPlaylistReady() when finished.
updateStatus()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 
add(Uri uri, String mime)106     public PlaylistItem add(Uri uri, String mime) {
107         return add(uri, mime, null);
108     }
109 
add(Uri uri, String mime, PendingIntent receiver)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 
remove(String iid)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 
seek(String iid, long pos)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 
getStatus(String iid)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 
pause()180     public void pause() {
181         if (DEBUG) {
182             log("pause");
183         }
184         mPaused = true;
185         updatePlaybackState();
186     }
187 
resume()188     public void resume() {
189         if (DEBUG) {
190             log("resume");
191         }
192         mPaused = false;
193         updatePlaybackState();
194     }
195 
stop()196     public void stop() {
197         if (DEBUG) {
198             log("stop");
199         }
200         mPlayer.stop();
201         mPlaylist.clear();
202         mPaused = false;
203         updateStatus();
204     }
205 
startSession()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 
endSession()217     public boolean endSession() {
218         if (mSessionValid) {
219             mSessionValid = false;
220             return true;
221         }
222         return false;
223     }
224 
getSessionStatus(String sid)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).
suspend(long pos)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.
unsuspend()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
onError()273     public void onError() {
274         finishItem(true);
275     }
276 
277     @Override
onCompletion()278     public void onCompletion() {
279         finishItem(false);
280     }
281 
282     @Override
onPlaylistChanged()283     public void onPlaylistChanged() {
284         // Playlist has changed, update the cached playlist
285         updateStatus();
286     }
287 
288     @Override
onPlaylistReady()289     public void onPlaylistReady() {
290         // Notify activity to update Ui
291         if (mCallback != null) {
292             mCallback.onStatusChanged();
293         }
294     }
295 
log(String message)296     private void log(String message) {
297         Log.d(TAG, mName + ": " + message);
298     }
299 
checkPlayer()300     private void checkPlayer() {
301         if (mPlayer == null) {
302             throw new IllegalStateException("Player not set!");
303         }
304     }
305 
checkSession()306     private void checkSession() {
307         if (!mSessionValid) {
308             throw new IllegalStateException("Session not set!");
309         }
310     }
311 
checkPlayerAndSession()312     private void checkPlayerAndSession() {
313         checkPlayer();
314         checkSession();
315     }
316 
checkItemCurrent(String iid)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 
updatePlaybackState()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 
removeItem(String iid, int state)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 
finishItem(boolean error)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
setPlayer(Player player)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
setCallback(Callback callback)398     public void setCallback(Callback callback) {
399         mCallback = callback;
400     }
401 
402     @Override
toString()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 {
onStatusChanged()416         void onStatusChanged();
onItemChanged(PlaylistItem item)417         void onItemChanged(PlaylistItem item);
418     }
419 }
420