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