1/* 2 * Copyright 2018 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 androidx.media; 18 19import android.annotation.TargetApi; 20import android.os.Build; 21 22import androidx.annotation.GuardedBy; 23import androidx.annotation.NonNull; 24import androidx.annotation.Nullable; 25import androidx.annotation.VisibleForTesting; 26import androidx.collection.ArrayMap; 27import androidx.media.MediaPlayerInterface.PlayerEventCallback; 28import androidx.media.MediaSession2.OnDataSourceMissingHelper; 29 30import java.util.ArrayList; 31import java.util.Collections; 32import java.util.List; 33import java.util.Map; 34 35@TargetApi(Build.VERSION_CODES.KITKAT) 36class SessionPlaylistAgentImplBase extends MediaPlaylistAgent { 37 @VisibleForTesting 38 static final int END_OF_PLAYLIST = -1; 39 @VisibleForTesting 40 static final int NO_VALID_ITEMS = -2; 41 42 private final PlayItem mEopPlayItem = new PlayItem(END_OF_PLAYLIST, null); 43 44 private final Object mLock = new Object(); 45 private final MediaSession2ImplBase mSession; 46 private final MyPlayerEventCallback mPlayerCallback; 47 48 @GuardedBy("mLock") 49 private MediaPlayerInterface mPlayer; 50 @GuardedBy("mLock") 51 private OnDataSourceMissingHelper mDsmHelper; 52 // TODO: Check if having the same item is okay (b/74090741) 53 @GuardedBy("mLock") 54 private ArrayList<MediaItem2> mPlaylist = new ArrayList<>(); 55 @GuardedBy("mLock") 56 private ArrayList<MediaItem2> mShuffledList = new ArrayList<>(); 57 @GuardedBy("mLock") 58 private Map<MediaItem2, DataSourceDesc> mItemDsdMap = new ArrayMap<>(); 59 @GuardedBy("mLock") 60 private MediaMetadata2 mMetadata; 61 @GuardedBy("mLock") 62 private int mRepeatMode; 63 @GuardedBy("mLock") 64 private int mShuffleMode; 65 @GuardedBy("mLock") 66 private PlayItem mCurrent; 67 68 // Called on session callback executor. 69 private class MyPlayerEventCallback extends PlayerEventCallback { 70 @Override 71 public void onCurrentDataSourceChanged(@NonNull MediaPlayerInterface mpb, 72 @Nullable DataSourceDesc dsd) { 73 synchronized (mLock) { 74 if (mPlayer != mpb) { 75 return; 76 } 77 if (dsd == null && mCurrent != null) { 78 mCurrent = getNextValidPlayItemLocked(mCurrent.shuffledIdx, 1); 79 updateCurrentIfNeededLocked(); 80 } 81 } 82 } 83 } 84 85 private class PlayItem { 86 public int shuffledIdx; 87 public DataSourceDesc dsd; 88 public MediaItem2 mediaItem; 89 90 PlayItem(int shuffledIdx) { 91 this(shuffledIdx, null); 92 } 93 94 PlayItem(int shuffledIdx, DataSourceDesc dsd) { 95 this.shuffledIdx = shuffledIdx; 96 if (shuffledIdx >= 0) { 97 this.mediaItem = mShuffledList.get(shuffledIdx); 98 if (dsd == null) { 99 synchronized (mLock) { 100 this.dsd = retrieveDataSourceDescLocked(this.mediaItem); 101 } 102 } else { 103 this.dsd = dsd; 104 } 105 } 106 } 107 108 @SuppressWarnings("ReferenceEquality") 109 boolean isValid() { 110 if (this == mEopPlayItem) { 111 return true; 112 } 113 if (mediaItem == null) { 114 return false; 115 } 116 if (dsd == null) { 117 return false; 118 } 119 if (mediaItem.getDataSourceDesc() != null 120 && !mediaItem.getDataSourceDesc().equals(dsd)) { 121 return false; 122 } 123 synchronized (mLock) { 124 if (shuffledIdx >= mShuffledList.size()) { 125 return false; 126 } 127 if (mediaItem != mShuffledList.get(shuffledIdx)) { 128 return false; 129 } 130 } 131 return true; 132 } 133 } 134 135 SessionPlaylistAgentImplBase(@NonNull MediaSession2ImplBase session, 136 @NonNull MediaPlayerInterface player) { 137 super(); 138 if (session == null) { 139 throw new IllegalArgumentException("sessionImpl shouldn't be null"); 140 } 141 if (player == null) { 142 throw new IllegalArgumentException("player shouldn't be null"); 143 } 144 mSession = session; 145 mPlayer = player; 146 mPlayerCallback = new MyPlayerEventCallback(); 147 mPlayer.registerPlayerEventCallback(mSession.getCallbackExecutor(), mPlayerCallback); 148 } 149 150 public void setPlayer(@NonNull MediaPlayerInterface player) { 151 if (player == null) { 152 throw new IllegalArgumentException("player shouldn't be null"); 153 } 154 synchronized (mLock) { 155 if (player == mPlayer) { 156 return; 157 } 158 mPlayer.unregisterPlayerEventCallback(mPlayerCallback); 159 mPlayer = player; 160 mPlayer.registerPlayerEventCallback(mSession.getCallbackExecutor(), mPlayerCallback); 161 updatePlayerDataSourceLocked(); 162 } 163 } 164 165 public void setOnDataSourceMissingHelper(OnDataSourceMissingHelper helper) { 166 synchronized (mLock) { 167 mDsmHelper = helper; 168 } 169 } 170 171 public void clearOnDataSourceMissingHelper() { 172 synchronized (mLock) { 173 mDsmHelper = null; 174 } 175 } 176 177 @Override 178 public @Nullable List<MediaItem2> getPlaylist() { 179 synchronized (mLock) { 180 return Collections.unmodifiableList(mPlaylist); 181 } 182 } 183 184 @Override 185 public void setPlaylist(@NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) { 186 if (list == null) { 187 throw new IllegalArgumentException("list shouldn't be null"); 188 } 189 190 synchronized (mLock) { 191 mItemDsdMap.clear(); 192 193 mPlaylist.clear(); 194 mPlaylist.addAll(list); 195 applyShuffleModeLocked(); 196 197 mMetadata = metadata; 198 mCurrent = getNextValidPlayItemLocked(END_OF_PLAYLIST, 1); 199 updatePlayerDataSourceLocked(); 200 } 201 notifyPlaylistChanged(); 202 } 203 204 @Override 205 public @Nullable MediaMetadata2 getPlaylistMetadata() { 206 synchronized (mLock) { 207 return mMetadata; 208 } 209 } 210 211 @Override 212 public void updatePlaylistMetadata(@Nullable MediaMetadata2 metadata) { 213 synchronized (mLock) { 214 if (metadata == mMetadata) { 215 return; 216 } 217 mMetadata = metadata; 218 } 219 notifyPlaylistMetadataChanged(); 220 } 221 222 @Override 223 public MediaItem2 getCurrentMediaItem() { 224 synchronized (mLock) { 225 return mCurrent == null ? null : mCurrent.mediaItem; 226 } 227 } 228 229 @Override 230 public void addPlaylistItem(int index, @NonNull MediaItem2 item) { 231 if (item == null) { 232 throw new IllegalArgumentException("item shouldn't be null"); 233 } 234 synchronized (mLock) { 235 index = clamp(index, mPlaylist.size()); 236 int shuffledIdx = index; 237 mPlaylist.add(index, item); 238 if (mShuffleMode == MediaPlaylistAgent.SHUFFLE_MODE_NONE) { 239 mShuffledList.add(index, item); 240 } else { 241 // Add the item in random position of mShuffledList. 242 shuffledIdx = (int) (Math.random() * (mShuffledList.size() + 1)); 243 mShuffledList.add(shuffledIdx, item); 244 } 245 if (!hasValidItem()) { 246 mCurrent = getNextValidPlayItemLocked(END_OF_PLAYLIST, 1); 247 updatePlayerDataSourceLocked(); 248 } else { 249 updateCurrentIfNeededLocked(); 250 } 251 } 252 notifyPlaylistChanged(); 253 } 254 255 @Override 256 public void removePlaylistItem(@NonNull MediaItem2 item) { 257 if (item == null) { 258 throw new IllegalArgumentException("item shouldn't be null"); 259 } 260 synchronized (mLock) { 261 if (!mPlaylist.remove(item)) { 262 return; 263 } 264 mShuffledList.remove(item); 265 mItemDsdMap.remove(item); 266 updateCurrentIfNeededLocked(); 267 } 268 notifyPlaylistChanged(); 269 } 270 271 @Override 272 public void replacePlaylistItem(int index, @NonNull MediaItem2 item) { 273 if (item == null) { 274 throw new IllegalArgumentException("item shouldn't be null"); 275 } 276 synchronized (mLock) { 277 if (mPlaylist.size() <= 0) { 278 return; 279 } 280 index = clamp(index, mPlaylist.size() - 1); 281 int shuffledIdx = mShuffledList.indexOf(mPlaylist.get(index)); 282 mItemDsdMap.remove(mShuffledList.get(shuffledIdx)); 283 mShuffledList.set(shuffledIdx, item); 284 mPlaylist.set(index, item); 285 if (!hasValidItem()) { 286 mCurrent = getNextValidPlayItemLocked(END_OF_PLAYLIST, 1); 287 updatePlayerDataSourceLocked(); 288 } else { 289 updateCurrentIfNeededLocked(); 290 } 291 } 292 notifyPlaylistChanged(); 293 } 294 295 @Override 296 public void skipToPlaylistItem(@NonNull MediaItem2 item) { 297 if (item == null) { 298 throw new IllegalArgumentException("item shouldn't be null"); 299 } 300 synchronized (mLock) { 301 if (!hasValidItem() || item.equals(mCurrent.mediaItem)) { 302 return; 303 } 304 int shuffledIdx = mShuffledList.indexOf(item); 305 if (shuffledIdx < 0) { 306 return; 307 } 308 mCurrent = new PlayItem(shuffledIdx); 309 updateCurrentIfNeededLocked(); 310 } 311 } 312 313 @Override 314 public void skipToPreviousItem() { 315 synchronized (mLock) { 316 if (!hasValidItem()) { 317 return; 318 } 319 PlayItem prev = getNextValidPlayItemLocked(mCurrent.shuffledIdx, -1); 320 if (prev != mEopPlayItem) { 321 mCurrent = prev; 322 } 323 updateCurrentIfNeededLocked(); 324 } 325 } 326 327 @Override 328 public void skipToNextItem() { 329 synchronized (mLock) { 330 if (!hasValidItem() || mCurrent == mEopPlayItem) { 331 return; 332 } 333 PlayItem next = getNextValidPlayItemLocked(mCurrent.shuffledIdx, 1); 334 if (next != mEopPlayItem) { 335 mCurrent = next; 336 } 337 updateCurrentIfNeededLocked(); 338 } 339 } 340 341 @Override 342 public int getRepeatMode() { 343 synchronized (mLock) { 344 return mRepeatMode; 345 } 346 } 347 348 @Override 349 @SuppressWarnings("FallThrough") 350 public void setRepeatMode(int repeatMode) { 351 if (repeatMode < MediaPlaylistAgent.REPEAT_MODE_NONE 352 || repeatMode > MediaPlaylistAgent.REPEAT_MODE_GROUP) { 353 return; 354 } 355 synchronized (mLock) { 356 if (mRepeatMode == repeatMode) { 357 return; 358 } 359 mRepeatMode = repeatMode; 360 switch (repeatMode) { 361 case MediaPlaylistAgent.REPEAT_MODE_ONE: 362 if (mCurrent != null && mCurrent != mEopPlayItem) { 363 mPlayer.loopCurrent(true); 364 } 365 break; 366 case MediaPlaylistAgent.REPEAT_MODE_ALL: 367 case MediaPlaylistAgent.REPEAT_MODE_GROUP: 368 if (mCurrent == mEopPlayItem) { 369 mCurrent = getNextValidPlayItemLocked(END_OF_PLAYLIST, 1); 370 updatePlayerDataSourceLocked(); 371 } 372 // Fall through 373 case MediaPlaylistAgent.REPEAT_MODE_NONE: 374 mPlayer.loopCurrent(false); 375 break; 376 } 377 } 378 notifyRepeatModeChanged(); 379 } 380 381 @Override 382 public int getShuffleMode() { 383 synchronized (mLock) { 384 return mShuffleMode; 385 } 386 } 387 388 @Override 389 public void setShuffleMode(int shuffleMode) { 390 if (shuffleMode < MediaPlaylistAgent.SHUFFLE_MODE_NONE 391 || shuffleMode > MediaPlaylistAgent.SHUFFLE_MODE_GROUP) { 392 return; 393 } 394 synchronized (mLock) { 395 if (mShuffleMode == shuffleMode) { 396 return; 397 } 398 mShuffleMode = shuffleMode; 399 applyShuffleModeLocked(); 400 updateCurrentIfNeededLocked(); 401 } 402 notifyShuffleModeChanged(); 403 } 404 405 @Override 406 public MediaItem2 getMediaItem(DataSourceDesc dsd) { 407 // TODO: implement this 408 return null; 409 } 410 411 @VisibleForTesting 412 int getCurShuffledIndex() { 413 synchronized (mLock) { 414 return hasValidItem() ? mCurrent.shuffledIdx : NO_VALID_ITEMS; 415 } 416 } 417 418 private boolean hasValidItem() { 419 synchronized (mLock) { 420 return mCurrent != null; 421 } 422 } 423 424 @SuppressWarnings("GuardedBy") 425 private DataSourceDesc retrieveDataSourceDescLocked(MediaItem2 item) { 426 DataSourceDesc dsd = item.getDataSourceDesc(); 427 if (dsd != null) { 428 mItemDsdMap.put(item, dsd); 429 return dsd; 430 } 431 dsd = mItemDsdMap.get(item); 432 if (dsd != null) { 433 return dsd; 434 } 435 OnDataSourceMissingHelper helper = mDsmHelper; 436 if (helper != null) { 437 // TODO: Do not call onDataSourceMissing with the lock (b/74090741). 438 dsd = helper.onDataSourceMissing(mSession.getInstance(), item); 439 if (dsd != null) { 440 mItemDsdMap.put(item, dsd); 441 } 442 } 443 return dsd; 444 } 445 446 // TODO: consider to call updateCurrentIfNeededLocked inside (b/74090741) 447 @SuppressWarnings("GuardedBy") 448 private PlayItem getNextValidPlayItemLocked(int curShuffledIdx, int direction) { 449 int size = mPlaylist.size(); 450 if (curShuffledIdx == END_OF_PLAYLIST) { 451 curShuffledIdx = (direction > 0) ? -1 : size; 452 } 453 for (int i = 0; i < size; i++) { 454 curShuffledIdx += direction; 455 if (curShuffledIdx < 0 || curShuffledIdx >= mPlaylist.size()) { 456 if (mRepeatMode == REPEAT_MODE_NONE) { 457 return (i == size - 1) ? null : mEopPlayItem; 458 } else { 459 curShuffledIdx = curShuffledIdx < 0 ? mPlaylist.size() - 1 : 0; 460 } 461 } 462 DataSourceDesc dsd = retrieveDataSourceDescLocked(mShuffledList.get(curShuffledIdx)); 463 if (dsd != null) { 464 return new PlayItem(curShuffledIdx, dsd); 465 } 466 } 467 return null; 468 } 469 470 @SuppressWarnings("GuardedBy") 471 private void updateCurrentIfNeededLocked() { 472 if (!hasValidItem() || mCurrent.isValid()) { 473 return; 474 } 475 int shuffledIdx = mShuffledList.indexOf(mCurrent.mediaItem); 476 if (shuffledIdx >= 0) { 477 // Added an item. 478 mCurrent.shuffledIdx = shuffledIdx; 479 return; 480 } 481 482 if (mCurrent.shuffledIdx >= mShuffledList.size()) { 483 mCurrent = getNextValidPlayItemLocked(mShuffledList.size() - 1, 1); 484 } else { 485 mCurrent.mediaItem = mShuffledList.get(mCurrent.shuffledIdx); 486 if (retrieveDataSourceDescLocked(mCurrent.mediaItem) == null) { 487 mCurrent = getNextValidPlayItemLocked(mCurrent.shuffledIdx, 1); 488 } 489 } 490 updatePlayerDataSourceLocked(); 491 return; 492 } 493 494 @SuppressWarnings("GuardedBy") 495 private void updatePlayerDataSourceLocked() { 496 if (mCurrent == null || mCurrent == mEopPlayItem) { 497 return; 498 } 499 if (mPlayer.getCurrentDataSource() != mCurrent.dsd) { 500 mPlayer.setDataSource(mCurrent.dsd); 501 mPlayer.loopCurrent(mRepeatMode == MediaPlaylistAgent.REPEAT_MODE_ONE); 502 } 503 // TODO: Call setNextDataSource (b/74090741) 504 } 505 506 @SuppressWarnings("GuardedBy") 507 private void applyShuffleModeLocked() { 508 mShuffledList.clear(); 509 mShuffledList.addAll(mPlaylist); 510 if (mShuffleMode == MediaPlaylistAgent.SHUFFLE_MODE_ALL 511 || mShuffleMode == MediaPlaylistAgent.SHUFFLE_MODE_GROUP) { 512 Collections.shuffle(mShuffledList); 513 } 514 } 515 516 // Clamps value to [0, size] 517 private static int clamp(int value, int size) { 518 if (value < 0) { 519 return 0; 520 } 521 return (value > size) ? size : value; 522 } 523} 524