Home | History | Annotate | Download | only in media
      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