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
}