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
17package com.example.android.supportv7.media;
18
19import android.app.PendingIntent;
20import android.net.Uri;
21import android.support.v7.media.MediaItemStatus;
22import android.support.v7.media.MediaSessionStatus;
23import android.util.Log;
24
25import java.util.List;
26import 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 */
36public 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.takeSnapshot();
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(String title, Uri uri, String mime) {
103        return add(title, uri, mime, null);
104    }
105
106    public PlaylistItem add(String title, Uri uri, String mime, PendingIntent receiver) {
107        if (DEBUG) {
108            log("add: title=" + title + ", 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(Integer.toString(mSessionId),
116                Integer.toString(mItemId), title, 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        } else {
353            mPlayer.initMediaSession();
354        }
355        updateStatus();
356    }
357
358    private PlaylistItem removeItem(String iid, int state) {
359        checkPlayerAndSession();
360        List<PlaylistItem> queue =
361                new ArrayList<PlaylistItem>(mPlaylist.size());
362        PlaylistItem found = null;
363        for (PlaylistItem item : mPlaylist) {
364            if (iid.equals(item.getItemId())) {
365                if (mPlayer.isQueuingSupported()) {
366                    mPlayer.remove(item.getRemoteItemId());
367                } else if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING
368                        || item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED){
369                    mPlayer.stop();
370                }
371                item.setState(state);
372                found = item;
373                // notify client that item is now removed
374                if (mCallback != null) {
375                    mCallback.onItemChanged(found);
376                }
377            } else {
378                queue.add(item);
379            }
380        }
381        if (found != null) {
382            mPlaylist = queue;
383            updatePlaybackState();
384        } else {
385            log("item not found");
386        }
387        return found;
388    }
389
390    private void finishItem(boolean error) {
391        PlaylistItem item = getCurrentItem();
392        if (item != null) {
393            removeItem(item.getItemId(), error ?
394                    MediaItemStatus.PLAYBACK_STATE_ERROR :
395                        MediaItemStatus.PLAYBACK_STATE_FINISHED);
396            updateStatus();
397        }
398    }
399
400    // set the Player that this playback manager will interact with
401    public void setPlayer(Player player) {
402        mPlayer = player;
403        checkPlayer();
404        mPlayer.setCallback(this);
405    }
406
407    // provide a callback interface to tell the UI when significant state changes occur
408    public void setCallback(Callback callback) {
409        mCallback = callback;
410    }
411
412    @Override
413    public String toString() {
414        String result = "Media Queue: ";
415        if (!mPlaylist.isEmpty()) {
416            for (PlaylistItem item : mPlaylist) {
417                result += "\n" + item.toString();
418            }
419        } else {
420            result += "<empty>";
421        }
422        return result;
423    }
424
425    public interface Callback {
426        void onStatusChanged();
427        void onItemChanged(PlaylistItem item);
428    }
429}
430