View Javadoc

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 		// populate the playlist
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 		// clean the history
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 		// if Random.ALBUM, remove all upcoming tracks
207 		if (random.groupsByAlbum()) {
208 			playlist.subList(firstUpcomingTrackIndex(), playlist.size())
209 					.clear();
210 			populatePlaylist(true);
211 		}
212 	}
213 
214 	/***
215 	 * Tracks &lt;{@link #currentTrack} + 1 .. {@link #currentTrack} +
216 	 * {@link #queuedCount}&gt; 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 		// remove the queued tracks from playlist - these tracks will be added
275 		// after last queued track.
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 		// append the tracks and assign them the correct queue number
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 			// set currentTrack to -1; we need to remove history to do that
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 }