1 /***
2 * Ambient - A music player for the Android platform
3 Copyright (C) 2007 Martin Vysny
4
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18 package sk.baka.ambient.playlist;
19
20 import java.io.Serializable;
21 import java.util.ArrayList;
22 import java.util.Collections;
23 import java.util.LinkedList;
24 import java.util.List;
25 import java.util.Map;
26
27 import sk.baka.ambient.collection.IDynamicPlaylistTrackProvider;
28 import sk.baka.ambient.collection.TrackMetadataBean;
29 import sk.baka.ambient.commons.Interval;
30
31 /***
32 * <p>
33 * A dynamic playlist - items are added to the front of the playlist and removed
34 * from the back of the playlist (if required). Does not support shuffle mode
35 * itself - it configures the {@link IDynamicPlaylistTrackProvider} to provide
36 * tracks in particular order instead.
37 * </p>
38 * <p>
39 * Queued tracks are lined always in front of a currently playing track.
40 * </p>
41 *
42 * @author Martin Vysny
43 */
44 public final class DynamicPlaylistStrategy implements IPlaylistStrategy,
45 Serializable {
46 /***
47 *
48 */
49 private static final long serialVersionUID = -8794527285381642344L;
50
51 /***
52 * Provides tracks for playing.
53 */
54 private final IDynamicPlaylistTrackProvider provider;
55
56 /***
57 * The playlist. Always contains at most {@link #historyLength} + 1 +
58 * {@link #upcomingTrackCount} tracks.
59 */
60 private final LinkedList<PlaylistItem> playlist = new LinkedList<PlaylistItem>();
61
62 /***
63 * Unmodifiable view on the {@link #playlist}.
64 */
65 private transient List<PlaylistItem> unmodifiablePlaylist;
66
67 /***
68 * Creates new dynamic playlist.
69 *
70 * @param provider
71 * provides tracks for playing.
72 * @param strategy
73 * if not <code>null</code> then clone this strategy.
74 * @param historyLength
75 * Number of entries in the history.
76 * @param upcomingTrackCount
77 * Number of upcoming tracks to show.
78 */
79 public DynamicPlaylistStrategy(
80 final IDynamicPlaylistTrackProvider provider,
81 final IPlaylistStrategy strategy, final int historyLength,
82 final int upcomingTrackCount) {
83 super();
84 this.provider = provider;
85 this.historyLength = historyLength;
86 this.upcomingTrackCount = upcomingTrackCount;
87 if (strategy != null) {
88 playlist.addAll(strategy.getPlayItems());
89 currentTrack = strategy.getCurrentlyPlaying();
90 final List<Integer> queued = strategy.getQueue();
91 queueIndices(queued);
92 setRandom(strategy.getRandom());
93 }
94 populatePlaylist(true);
95 }
96
97 /***
98 * Number of entries in the history.
99 */
100 private transient int historyLength = 5;
101
102 /***
103 * Number of upcoming tracks to show.
104 */
105 private transient int upcomingTrackCount = 10;
106
107 /***
108 * Sets the history length.
109 *
110 * @param historyLength
111 * how many tracks are kept as a history after playback. Cannot
112 * be negative.
113 */
114 public void setHistoryLength(final int historyLength) {
115 this.historyLength = historyLength;
116 populatePlaylist(true);
117 }
118
119 /***
120 * Sets the upcoming track count.
121 *
122 * @param upcomingTrackCount
123 * how many tracks will be shown before the playing track. Cannot
124 * be negative.
125 */
126 public void setUpcomingTrackCount(final int upcomingTrackCount) {
127 this.upcomingTrackCount = upcomingTrackCount;
128 populatePlaylist(true);
129 }
130
131 /***
132 * Currently played song. Index to the {@link #playlist} list.
133 * <code>-1</code> if no song is currently being played.
134 */
135 private int currentTrack = -1;
136
137 private Random random = Random.NONE;
138
139 /***
140 * Polls the provider until the playlist is populated as required. Clears
141 * the history as well, if required.
142 *
143 * @param cleanHistory
144 * remove old tracks until there is no more than
145 * {@link #historyLength} tracks in the history.
146 */
147 private void populatePlaylist(final boolean cleanHistory) {
148
149 while (size() - currentTrack - 1 < upcomingTrackCount) {
150 if (!provider.hasNext()) {
151 break;
152 }
153 final TrackMetadataBean track = provider.next();
154 final PlaylistItem item = new PlaylistItem(track, 0, 0);
155 playlist.add(item);
156 }
157
158 if (cleanHistory) {
159 final int currentHistoryLength = currentTrack;
160 if (currentHistoryLength > historyLength) {
161 playlist.subList(0, currentHistoryLength - historyLength)
162 .clear();
163 provider
164 .removeFromHistory(currentHistoryLength - historyLength);
165 currentTrack = historyLength;
166 }
167 }
168 }
169
170 public List<PlaylistItem> getPlayItems() {
171 if (unmodifiablePlaylist == null) {
172 unmodifiablePlaylist = Collections.unmodifiableList(playlist);
173 }
174 return unmodifiablePlaylist;
175 }
176
177 public int size() {
178 return playlist.size();
179 }
180
181 public void shuffle() {
182 playlist.subList(firstUpcomingTrackIndex(), playlist.size()).clear();
183 populatePlaylist(true);
184 }
185
186 public void sortByAlbumOrder() {
187 Utils.sortPlaylistByAlbumOrder(playlist.subList(
188 firstUpcomingTrackIndex(), playlist.size()));
189 }
190
191 public void add(int index, List<TrackMetadataBean> tracks) {
192 int i = index;
193 final int firstUpcomingTrackIndex = firstUpcomingTrackIndex();
194 if (i < firstUpcomingTrackIndex)
195 i = firstUpcomingTrackIndex;
196 for (final TrackMetadataBean track : tracks) {
197 playlist.add(i++, new PlaylistItem(track, 0, 0));
198 }
199 }
200
201 public void setRandom(Random random) {
202 final TrackMetadataBean current = currentTrack < 0 ? null : playlist
203 .get(currentTrack).getTrack();
204 provider.setRandom(random, current);
205 this.random = random;
206
207 if (random.groupsByAlbum()) {
208 playlist.subList(firstUpcomingTrackIndex(), playlist.size())
209 .clear();
210 populatePlaylist(true);
211 }
212 }
213
214 /***
215 * Tracks <{@link #currentTrack} + 1 .. {@link #currentTrack} +
216 * {@link #queuedCount}> are currently queued.
217 */
218 private int queuedCount = 0;
219
220 public List<Integer> getQueue() {
221 final List<Integer> result = new ArrayList<Integer>(queuedCount);
222 final int firstUpcomingTrackIndex = firstUpcomingTrackIndex();
223 for (int i = currentTrack + 1; i < firstUpcomingTrackIndex; i++) {
224 result.add(i);
225 }
226 return result;
227 }
228
229 public void clearQueue() {
230 final int firstUpcomingTrackIndex = firstUpcomingTrackIndex();
231 for (int i = currentTrack + 1; i < firstUpcomingTrackIndex; i++) {
232 playlist.get(i).queueOrder = 0;
233 }
234 queuedCount = 0;
235 }
236
237 public int getCurrentlyPlaying() {
238 return currentTrack;
239 }
240
241 private int firstUpcomingTrackIndex() {
242 return currentTrack + 1 + queuedCount;
243 }
244
245 public void dequeue(int track) {
246 dequeue(track, false);
247 }
248
249 private void dequeue(int track, boolean remove) {
250 final PlaylistItem item = playlist.get(track);
251 if (item.queueOrder == 0)
252 return;
253 if (!remove) {
254 playlist.add(firstUpcomingTrackIndex(), item);
255 }
256 item.queueOrder = 0;
257 playlist.remove(track);
258 queuedCount--;
259 final int firstUpcomingTrackIndex = firstUpcomingTrackIndex();
260 for (int i = track; i < firstUpcomingTrackIndex; i++) {
261 playlist.get(i).queueOrder--;
262 }
263 }
264
265 /***
266 * Queues tracks with given indices.
267 *
268 * @param indices
269 * the tracks to queue.
270 */
271 private void queueIndices(final Iterable<? extends Integer> indices) {
272 final List<PlaylistItem> queue = new ArrayList<PlaylistItem>();
273 final List<PlaylistItem> toRemove = new ArrayList<PlaylistItem>();
274
275
276 for (final Integer index : indices) {
277 PlaylistItem item = playlist.get(index);
278 if (item.queueOrder != 0)
279 continue;
280 if (index.intValue() == currentTrack)
281 item = new PlaylistItem(item.getTrack(), 0, 0);
282 queue.add(item);
283 if (index.intValue() != currentTrack) {
284 toRemove.add(item);
285 if (index < currentTrack)
286 currentTrack--;
287 }
288 }
289 playlist.removeAll(toRemove);
290
291 playlist.addAll(firstUpcomingTrackIndex(), queue);
292 for (final PlaylistItem item : queue) {
293 queuedCount++;
294 item.queueOrder = queuedCount;
295 }
296 }
297
298 public void queue(final Interval interval) {
299 queueIndices(interval);
300 }
301
302 public void reinit() {
303 playlist.clear();
304 currentTrack = -1;
305 provider.removeFromHistory(Integer.MAX_VALUE);
306 populatePlaylist(true);
307 }
308
309 public int peekNext() {
310 final int result = currentTrack + 1;
311 if (result >= playlist.size()) {
312 return -1;
313 }
314 return result;
315 }
316
317 public int next() {
318 currentTrack++;
319 for (int i = currentTrack; i < currentTrack + queuedCount; i++) {
320 playlist.get(i).queueOrder--;
321 }
322 if (queuedCount > 0) {
323 queuedCount--;
324 }
325 populatePlaylist(true);
326 return currentTrack;
327 }
328
329 public int play(int track) {
330 if (track != -1) {
331 final PlaylistItem item = playlist.get(track);
332 item.playCount++;
333 }
334 if (track == currentTrack)
335 return track;
336 if (track == -1) {
337 playlist.subList(0, currentTrack).clear();
338 currentTrack = -1;
339 } else {
340 final PlaylistItem item = playlist.get(track);
341 remove(Interval.fromItem(track));
342 playlist.add(currentTrack + 1, item);
343 currentTrack++;
344 }
345 return currentTrack;
346 }
347
348 public int previous() {
349 if (currentTrack != -1) {
350 final PlaylistItem item = playlist.get(currentTrack);
351 item.playCount++;
352 }
353 return currentTrack;
354 }
355
356 public void remove(final Interval interval) {
357 if (interval.contains(currentTrack)) {
358
359 currentTrack = -1;
360 remove(new Interval(0, interval.end));
361 } else {
362 for (int index = interval.end; index >= interval.start; index--) {
363 final PlaylistItem item = playlist.get(index);
364 if (item.getQueueOrder() != 0) {
365 dequeue(index, true);
366 } else {
367 playlist.remove(index);
368 if (index < currentTrack)
369 currentTrack--;
370 }
371 }
372 }
373 populatePlaylist(true);
374 }
375
376 public Random getRandom() {
377 return random;
378 }
379
380 public Interval move(final Interval i, final int ti) {
381 if (i.end >= playlist.size()) {
382 throw new IllegalArgumentException("Interval " + i
383 + " exceeds playlist: " + playlist.size());
384 }
385 if ((ti < 0) || (ti > playlist.size()))
386 throw new IllegalArgumentException("Invalid targetIndex: " + ti);
387 Object result = i.subtract(getPlayedOrQueued());
388 final Interval[] ints;
389 if (result instanceof Interval) {
390 ints = new Interval[] { (Interval) result };
391 if (ints[0].isEmpty())
392 return i;
393 } else {
394 ints = (Interval[]) result;
395 }
396 int targetIndex = ti;
397 final List<PlaylistItem> movedTracks = new ArrayList<PlaylistItem>();
398 for (int intsIndex = ints.length - 1; intsIndex >= 0; intsIndex--) {
399 final Interval cut = ints[intsIndex];
400 final List<PlaylistItem> cutTracks = playlist.subList(cut.start,
401 cut.end + 1);
402 movedTracks.addAll(0, cutTracks);
403 if (currentTrack > cut.end) {
404 currentTrack -= cut.length;
405 }
406 if (targetIndex > cut.end) {
407 targetIndex -= cut.length;
408 }
409 cutTracks.clear();
410 }
411 playlist.addAll(targetIndex, movedTracks);
412 return new Interval(targetIndex, movedTracks.size());
413 }
414
415 private Interval getPlayedOrQueued() {
416 int first = currentTrack;
417 int length = 1 + queuedCount;
418 if (currentTrack < 0) {
419 first = 0;
420 length--;
421 }
422 return new Interval(first, length);
423 }
424
425 /***
426 * Returns current history size.
427 *
428 * @return number of items in the history.
429 */
430 public int getCurrentHistoryLength() {
431 return currentTrack < 0 ? 0 : currentTrack;
432 }
433
434 public Interval moveBy(final Interval interval, final int delta) {
435 if (interval.end >= playlist.size()) {
436 throw new IllegalArgumentException("Interval " + interval
437 + " exceeds playlist: " + playlist.size());
438 }
439 if (delta == 0)
440 return interval;
441 final boolean down = delta > 0;
442 int target = !down ? interval.start : interval.end + 1;
443 target += delta;
444 final Interval playedOrQueued = getPlayedOrQueued();
445 if (playedOrQueued.contains(target) && (target != playedOrQueued.start)) {
446 target = down ? playedOrQueued.end + 1 : playedOrQueued.start;
447 }
448 if ((target < 0) || (target > size()))
449 return interval;
450 return move(interval, target);
451 }
452
453 public void replaceLocations(Map<String, String> locationMap) {
454 Utils.replaceLocations(playlist, locationMap);
455 }
456 }