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