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 com.android.media;
18
19import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.media.DataSourceDesc;
22import android.media.MediaItem2;
23import android.media.MediaMetadata2;
24import android.media.MediaPlayerBase;
25import android.media.MediaPlayerBase.PlayerEventCallback;
26import android.media.MediaPlaylistAgent;
27import android.media.MediaSession2.OnDataSourceMissingHelper;
28import android.util.ArrayMap;
29
30import com.android.internal.annotations.GuardedBy;
31import com.android.internal.annotations.VisibleForTesting;
32
33import java.util.ArrayList;
34import java.util.Collections;
35import java.util.List;
36import java.util.Map;
37import java.util.concurrent.ThreadLocalRandom;
38
39public 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