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