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.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