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 }