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 androidx.media;
     18 
     19 import android.annotation.TargetApi;
     20 import android.os.Build;
     21 
     22 import androidx.annotation.GuardedBy;
     23 import androidx.annotation.NonNull;
     24 import androidx.annotation.Nullable;
     25 import androidx.annotation.VisibleForTesting;
     26 import androidx.collection.ArrayMap;
     27 import androidx.media.MediaPlayerInterface.PlayerEventCallback;
     28 import androidx.media.MediaSession2.OnDataSourceMissingHelper;
     29 
     30 import java.util.ArrayList;
     31 import java.util.Collections;
     32 import java.util.List;
     33 import java.util.Map;
     34 
     35 @TargetApi(Build.VERSION_CODES.KITKAT)
     36 class 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