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