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 static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
20
21import android.util.Log;
22
23import androidx.annotation.GuardedBy;
24import androidx.annotation.IntDef;
25import androidx.annotation.NonNull;
26import androidx.annotation.Nullable;
27import androidx.annotation.RestrictTo;
28import androidx.collection.SimpleArrayMap;
29
30import java.lang.annotation.Retention;
31import java.lang.annotation.RetentionPolicy;
32import java.util.List;
33import java.util.concurrent.Executor;
34
35/**
36 * MediaPlaylistAgent is the abstract class an application needs to derive from to pass an object
37 * to a MediaSession2 that will override default playlist handling behaviors. It contains a set of
38 * notify methods to signal MediaSession2 that playlist-related state has changed.
39 * <p>
40 * Playlists are composed of one or multiple {@link MediaItem2} instances, which combine metadata
41 * and data sources (as {@link DataSourceDesc})
42 * Used by {@link MediaSession2} and {@link MediaController2}.
43 */
44// This class only includes methods that contain {@link MediaItem2}.
45public abstract class MediaPlaylistAgent {
46    private static final String TAG = "MediaPlaylistAgent";
47
48    /**
49     * @hide
50     */
51    @RestrictTo(LIBRARY_GROUP)
52    @IntDef({REPEAT_MODE_NONE, REPEAT_MODE_ONE, REPEAT_MODE_ALL,
53            REPEAT_MODE_GROUP})
54    @Retention(RetentionPolicy.SOURCE)
55    public @interface RepeatMode {}
56
57    /**
58     * Playback will be stopped at the end of the playing media list.
59     */
60    public static final int REPEAT_MODE_NONE = 0;
61
62    /**
63     * Playback of the current playing media item will be repeated.
64     */
65    public static final int REPEAT_MODE_ONE = 1;
66
67    /**
68     * Playing media list will be repeated.
69     */
70    public static final int REPEAT_MODE_ALL = 2;
71
72    /**
73     * Playback of the playing media group will be repeated.
74     * A group is a logical block of media items which is specified in the section 5.7 of the
75     * Bluetooth AVRCP 1.6. An example of a group is the playlist.
76     */
77    public static final int REPEAT_MODE_GROUP = 3;
78
79    /**
80     * @hide
81     */
82    @RestrictTo(LIBRARY_GROUP)
83    @IntDef({SHUFFLE_MODE_NONE, SHUFFLE_MODE_ALL, SHUFFLE_MODE_GROUP})
84    @Retention(RetentionPolicy.SOURCE)
85    public @interface ShuffleMode {}
86
87    /**
88     * Media list will be played in order.
89     */
90    public static final int SHUFFLE_MODE_NONE = 0;
91
92    /**
93     * Media list will be played in shuffled order.
94     */
95    public static final int SHUFFLE_MODE_ALL = 1;
96
97    /**
98     * Media group will be played in shuffled order.
99     * A group is a logical block of media items which is specified in the section 5.7 of the
100     * Bluetooth AVRCP 1.6. An example of a group is the playlist.
101     */
102    public static final int SHUFFLE_MODE_GROUP = 2;
103
104    private final Object mLock = new Object();
105    @GuardedBy("mLock")
106    private final SimpleArrayMap<PlaylistEventCallback, Executor> mCallbacks =
107            new SimpleArrayMap<>();
108
109    /**
110     * Register {@link PlaylistEventCallback} to listen changes in the underlying
111     * {@link MediaPlaylistAgent}.
112     *
113     * @param executor a callback Executor
114     * @param callback a PlaylistEventCallback
115     * @throws IllegalArgumentException if executor or callback is {@code null}.
116     */
117    public final void registerPlaylistEventCallback(
118            @NonNull /*@CallbackExecutor*/ Executor executor,
119            @NonNull PlaylistEventCallback callback) {
120        if (executor == null) {
121            throw new IllegalArgumentException("executor shouldn't be null");
122        }
123        if (callback == null) {
124            throw new IllegalArgumentException("callback shouldn't be null");
125        }
126
127        synchronized (mLock) {
128            if (mCallbacks.get(callback) != null) {
129                Log.w(TAG, "callback is already added. Ignoring.");
130                return;
131            }
132            mCallbacks.put(callback, executor);
133        }
134    }
135
136    /**
137     * Unregister the previously registered {@link PlaylistEventCallback}.
138     *
139     * @param callback the callback to be removed
140     * @throws IllegalArgumentException if the callback is {@code null}.
141     */
142    public final void unregisterPlaylistEventCallback(@NonNull PlaylistEventCallback callback) {
143        if (callback == null) {
144            throw new IllegalArgumentException("callback shouldn't be null");
145        }
146        synchronized (mLock) {
147            mCallbacks.remove(callback);
148        }
149    }
150
151    /**
152     * Notifies the current playlist and playlist metadata. Call this API when the playlist is
153     * changed.
154     * <p>
155     * Registered {@link PlaylistEventCallback} would receive this event through the
156     * {@link PlaylistEventCallback#onPlaylistChanged(MediaPlaylistAgent, List, MediaMetadata2)}.
157     */
158    public final void notifyPlaylistChanged() {
159        SimpleArrayMap<PlaylistEventCallback, Executor> callbacks = getCallbacks();
160        final List<MediaItem2> playlist = getPlaylist();
161        final MediaMetadata2 metadata = getPlaylistMetadata();
162        for (int i = 0; i < callbacks.size(); i++) {
163            final PlaylistEventCallback callback = callbacks.keyAt(i);
164            final Executor executor = callbacks.valueAt(i);
165            executor.execute(new Runnable() {
166                @Override
167                public void run() {
168                    callback.onPlaylistChanged(
169                            MediaPlaylistAgent.this, playlist, metadata);
170                }
171            });
172        }
173    }
174
175    /**
176     * Notifies the current playlist metadata. Call this API when the playlist metadata is changed.
177     * <p>
178     * Registered {@link PlaylistEventCallback} would receive this event through the
179     * {@link PlaylistEventCallback#onPlaylistMetadataChanged(MediaPlaylistAgent, MediaMetadata2)}.
180     */
181    public final void notifyPlaylistMetadataChanged() {
182        SimpleArrayMap<PlaylistEventCallback, Executor> callbacks = getCallbacks();
183        for (int i = 0; i < callbacks.size(); i++) {
184            final PlaylistEventCallback callback = callbacks.keyAt(i);
185            final Executor executor = callbacks.valueAt(i);
186            executor.execute(new Runnable() {
187                @Override
188                public void run() {
189                    callback.onPlaylistMetadataChanged(
190                            MediaPlaylistAgent.this, MediaPlaylistAgent.this.getPlaylistMetadata());
191                }
192            });
193        }
194    }
195
196    /**
197     * Notifies the current shuffle mode. Call this API when the shuffle mode is changed.
198     * <p>
199     * Registered {@link PlaylistEventCallback} would receive this event through the
200     * {@link PlaylistEventCallback#onShuffleModeChanged(MediaPlaylistAgent, int)}.
201     */
202    public final void notifyShuffleModeChanged() {
203        SimpleArrayMap<PlaylistEventCallback, Executor> callbacks = getCallbacks();
204        for (int i = 0; i < callbacks.size(); i++) {
205            final PlaylistEventCallback callback = callbacks.keyAt(i);
206            final Executor executor = callbacks.valueAt(i);
207            executor.execute(new Runnable() {
208                @Override
209                public void run() {
210                    callback.onShuffleModeChanged(
211                            MediaPlaylistAgent.this, MediaPlaylistAgent.this.getShuffleMode());
212                }
213            });
214        }
215    }
216
217    /**
218     * Notifies the current repeat mode. Call this API when the repeat mode is changed.
219     * <p>
220     * Registered {@link PlaylistEventCallback} would receive this event through the
221     * {@link PlaylistEventCallback#onRepeatModeChanged(MediaPlaylistAgent, int)}.
222     */
223    public final void notifyRepeatModeChanged() {
224        SimpleArrayMap<PlaylistEventCallback, Executor> callbacks = getCallbacks();
225        for (int i = 0; i < callbacks.size(); i++) {
226            final PlaylistEventCallback callback = callbacks.keyAt(i);
227            final Executor executor = callbacks.valueAt(i);
228            executor.execute(new Runnable() {
229                @Override
230                public void run() {
231                    callback.onRepeatModeChanged(
232                            MediaPlaylistAgent.this, MediaPlaylistAgent.this.getRepeatMode());
233                }
234            });
235        }
236    }
237
238    /**
239     * Returns the playlist
240     *
241     * @return playlist, or null if none is set.
242     */
243    public abstract @Nullable List<MediaItem2> getPlaylist();
244
245    /**
246     * Sets the playlist with the metadata.
247     * <p>
248     * When the playlist is changed, call {@link #notifyPlaylistChanged()} to notify changes to the
249     * registered callbacks.
250     *
251     * @param list playlist
252     * @param metadata metadata of the playlist
253     * @see #notifyPlaylistChanged()
254     */
255    public abstract void setPlaylist(@NonNull List<MediaItem2> list,
256            @Nullable MediaMetadata2 metadata);
257
258    /**
259     * Returns the playlist metadata
260     *
261     * @return metadata metadata of the playlist, or null if none is set
262     */
263    public abstract @Nullable MediaMetadata2 getPlaylistMetadata();
264
265    /**
266     * Updates the playlist metadata.
267     * <p>
268     * When the playlist metadata is changed, call {@link #notifyPlaylistMetadataChanged()} to
269     * notify changes to the registered callbacks.
270     *
271     * @param metadata metadata of the playlist
272     * @see #notifyPlaylistMetadataChanged()
273     */
274    public abstract void updatePlaylistMetadata(@Nullable MediaMetadata2 metadata);
275
276    /**
277     * Returns currently playing media item.
278     */
279    public abstract MediaItem2 getCurrentMediaItem();
280
281    /**
282     * Adds the media item to the playlist at position index. Index equals or greater than
283     * the current playlist size (e.g. {@link Integer#MAX_VALUE}) will add the item at the end of
284     * the playlist.
285     * <p>
286     * This will not change the currently playing media item.
287     * If index is less than or equal to the current index of the playlist,
288     * the current index of the playlist will be incremented correspondingly.
289     *
290     * @param index the index you want to add
291     * @param item the media item you want to add
292     */
293    public abstract void addPlaylistItem(int index, @NonNull MediaItem2 item);
294
295    /**
296     * Removes the media item from the playlist
297     *
298     * @param item media item to remove
299     */
300    public abstract void removePlaylistItem(@NonNull MediaItem2 item);
301
302    /**
303     * Replace the media item at index in the playlist. This can be also used to update metadata of
304     * an item.
305     *
306     * @param index the index of the item to replace
307     * @param item the new item
308     */
309    public abstract void replacePlaylistItem(int index, @NonNull MediaItem2 item);
310
311    /**
312     * Skips to the the media item, and plays from it.
313     *
314     * @param item media item to start playing from
315     */
316    public abstract void skipToPlaylistItem(@NonNull MediaItem2 item);
317
318    /**
319     * Skips to the previous item in the playlist.
320     */
321    public abstract void skipToPreviousItem();
322
323    /**
324     * Skips to the next item in the playlist.
325     */
326    public abstract void skipToNextItem();
327
328    /**
329     * Gets the repeat mode
330     *
331     * @return repeat mode
332     * @see #REPEAT_MODE_NONE
333     * @see #REPEAT_MODE_ONE
334     * @see #REPEAT_MODE_ALL
335     * @see #REPEAT_MODE_GROUP
336     */
337    public abstract @RepeatMode int getRepeatMode();
338
339    /**
340     * Sets the repeat mode.
341     * <p>
342     * When the repeat mode is changed, call {@link #notifyRepeatModeChanged()} to notify changes
343     * to the registered callbacks.
344     *
345     * @param repeatMode repeat mode
346     * @see #REPEAT_MODE_NONE
347     * @see #REPEAT_MODE_ONE
348     * @see #REPEAT_MODE_ALL
349     * @see #REPEAT_MODE_GROUP
350     * @see #notifyRepeatModeChanged()
351     */
352    public abstract void setRepeatMode(@RepeatMode int repeatMode);
353
354    /**
355     * Gets the shuffle mode
356     *
357     * @return The shuffle mode
358     * @see #SHUFFLE_MODE_NONE
359     * @see #SHUFFLE_MODE_ALL
360     * @see #SHUFFLE_MODE_GROUP
361     */
362    public abstract @ShuffleMode int getShuffleMode();
363
364    /**
365     * Sets the shuffle mode.
366     * <p>
367     * When the shuffle mode is changed, call {@link #notifyShuffleModeChanged()} to notify changes
368     * to the registered callbacks.
369     *
370     * @param shuffleMode The shuffle mode
371     * @see #SHUFFLE_MODE_NONE
372     * @see #SHUFFLE_MODE_ALL
373     * @see #SHUFFLE_MODE_GROUP
374     * @see #notifyShuffleModeChanged()
375     */
376    public abstract void setShuffleMode(@ShuffleMode int shuffleMode);
377
378    /**
379     * Called by {@link MediaSession2} when it wants to translate {@link DataSourceDesc} from the
380     * {@link MediaPlayerInterface.PlayerEventCallback} to the {@link MediaItem2}. Override this
381     * method if you want to create {@link DataSourceDesc}s dynamically, instead of specifying them
382     * with {@link #setPlaylist(List, MediaMetadata2)}.
383     * <p>
384     * Session would throw an exception if this returns {@code null} for the dsd from the
385     * {@link MediaPlayerInterface.PlayerEventCallback}.
386     * <p>
387     * Default implementation calls the {@link #getPlaylist()} and searches the {@link MediaItem2}
388     * with the {@param dsd}.
389     *
390     * @param dsd The dsd to query
391     * @return A {@link MediaItem2} object in the playlist that matches given {@code dsd}.
392     * @throws IllegalArgumentException if {@code dsd} is null
393     */
394    public @Nullable MediaItem2 getMediaItem(@NonNull DataSourceDesc dsd) {
395        if (dsd == null) {
396            throw new IllegalArgumentException("dsd shouldn't be null");
397        }
398        List<MediaItem2> itemList = getPlaylist();
399        if (itemList == null) {
400            return null;
401        }
402        for (int i = 0; i < itemList.size(); i++) {
403            MediaItem2 item = itemList.get(i);
404            if (item != null && item.getDataSourceDesc().equals(dsd)) {
405                return item;
406            }
407        }
408        return null;
409    }
410
411    private SimpleArrayMap<PlaylistEventCallback, Executor> getCallbacks() {
412        SimpleArrayMap<PlaylistEventCallback, Executor> callbacks = new SimpleArrayMap<>();
413        synchronized (mLock) {
414            callbacks.putAll(mCallbacks);
415        }
416        return callbacks;
417    }
418
419    /**
420     * A callback class to receive notifications for events on the media player. See
421     * {@link MediaPlaylistAgent#registerPlaylistEventCallback(Executor, PlaylistEventCallback)}
422     * to register this callback.
423     */
424    public abstract static class PlaylistEventCallback {
425        /**
426         * Called when a playlist is changed.
427         *
428         * @param playlistAgent playlist agent for this event
429         * @param list new playlist
430         * @param metadata new metadata
431         */
432        public void onPlaylistChanged(@NonNull MediaPlaylistAgent playlistAgent,
433                @NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) { }
434
435        /**
436         * Called when a playlist metadata is changed.
437         *
438         * @param playlistAgent playlist agent for this event
439         * @param metadata new metadata
440         */
441        public void onPlaylistMetadataChanged(@NonNull MediaPlaylistAgent playlistAgent,
442                @Nullable MediaMetadata2 metadata) { }
443
444        /**
445         * Called when the shuffle mode is changed.
446         *
447         * @param playlistAgent playlist agent for this event
448         * @param shuffleMode repeat mode
449         * @see #SHUFFLE_MODE_NONE
450         * @see #SHUFFLE_MODE_ALL
451         * @see #SHUFFLE_MODE_GROUP
452         */
453        public void onShuffleModeChanged(@NonNull MediaPlaylistAgent playlistAgent,
454                @ShuffleMode int shuffleMode) { }
455
456        /**
457         * Called when the repeat mode is changed.
458         *
459         * @param playlistAgent playlist agent for this event
460         * @param repeatMode repeat mode
461         * @see #REPEAT_MODE_NONE
462         * @see #REPEAT_MODE_ONE
463         * @see #REPEAT_MODE_ALL
464         * @see #REPEAT_MODE_GROUP
465         */
466        public void onRepeatModeChanged(@NonNull MediaPlaylistAgent playlistAgent,
467                @RepeatMode int repeatMode) { }
468    }
469}
470