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;
19
20 import java.util.Collections;
21 import java.util.List;
22 import java.util.Map;
23
24 import sk.baka.ambient.AppState.IAppStateAware;
25 import sk.baka.ambient.collection.IDynamicPlaylistTrackProvider;
26 import sk.baka.ambient.collection.TrackMetadataBean;
27 import sk.baka.ambient.collection.TrackOriginEnum;
28 import sk.baka.ambient.collection.local.MediaStoreTrackProvider;
29 import sk.baka.ambient.commons.Interval;
30 import sk.baka.ambient.playerservice.IPlayer;
31 import sk.baka.ambient.playerservice.IPlayerListener;
32 import sk.baka.ambient.playerservice.PlayerService;
33 import sk.baka.ambient.playerservice.PlayerStateEnum;
34 import sk.baka.ambient.playlist.DynamicPlaylistStrategy;
35 import sk.baka.ambient.playlist.IPlaylistStrategy;
36 import sk.baka.ambient.playlist.PlaylistItem;
37 import sk.baka.ambient.playlist.Random;
38 import sk.baka.ambient.playlist.Repeat;
39 import sk.baka.ambient.playlist.StaticPlaylistStrategy;
40 import sk.baka.ambient.playlist.Utils;
41 import android.content.ComponentName;
42 import android.content.Context;
43 import android.content.Intent;
44 import android.content.ServiceConnection;
45 import android.os.Handler;
46 import android.os.IBinder;
47
48 /***
49 * This is THE component :) Combines player and playlist. Emits
50 * {@link IPlaylistPlayerListener} messages.
51 *
52 * @author Martin Vysny
53 */
54 public final class PlaylistPlayer implements IPlaylistStrategy, IAppStateAware {
55 /***
56 * The player service invocator.
57 */
58 IPlayer playerService;
59
60 /***
61 * Receives value of {@link #playerService}.
62 */
63 private final ServiceConnection serviceConnection = new ServiceConnection() {
64 public void onServiceConnected(ComponentName name, IBinder service) {
65 playerService = IPlayer.Stub.asInterface(service);
66
67 final AppState state = app.getStateHandler().getStartupState();
68 reinit(state);
69 }
70
71 public void onServiceDisconnected(ComponentName arg0) {
72
73 }
74 };
75
76 /***
77 * Creates new player instance.
78 *
79 * @param app
80 * the application instance.
81 */
82 PlaylistPlayer(final AmbientApplication app) {
83 app.bindService(new Intent(app, PlayerService.class),
84 serviceConnection, Context.BIND_AUTO_CREATE);
85 app.getBus().addHandler(listener);
86 this.app = app;
87 errHandler = new PlaylistPlayerErrorHandler(this);
88 }
89
90 private final AmbientApplication app;
91
92 private final PlaylistPlayerErrorHandler errHandler;
93
94 /***
95 * Stops the playback. Does nothing if the player is inactive.
96 */
97 public void stop() {
98 try {
99 playerService.stop();
100 } catch (Exception e) {
101 throw new RuntimeException(e);
102 }
103 errHandler.cancelLocationResolving();
104 invokePlaybackStateChanged();
105 }
106
107 /***
108 * Returns the playback state.
109 *
110 * @return the playback state.
111 */
112 public PlayerStateEnum getPlaybackState() {
113 if (playerService == null) {
114
115
116 return PlayerStateEnum.Stopped;
117 }
118 try {
119 return PlayerStateEnum.values()[playerService.getPlaybackState()];
120 } catch (Exception e) {
121 throw new RuntimeException(e);
122 }
123 }
124
125 /***
126 * Pauses the player, or resumes playback if the player was paused. Does
127 * nothing if the player is stopped.
128 */
129 public void pause() {
130 try {
131 playerService.pause();
132 } catch (Exception e) {
133 throw new RuntimeException(e);
134 }
135 invokePlaybackStateChanged();
136 }
137
138 /***
139 * Seeks to given position. If the position is invalid then the result is
140 * undefined. Does nothing if the playback is stopped.
141 *
142 * @param position
143 * Seek to this position in milliseconds.
144 */
145 public void seek(int position) {
146 try {
147 final boolean seekChanged = playerService.seek(position);
148 if (seekChanged) {
149 getBusInvocator().trackPositionChanged(position,
150 getPlaybackState() == PlayerStateEnum.Playing);
151 }
152 } catch (Exception e) {
153 throw new RuntimeException(e);
154 }
155 }
156
157 /***
158 * The playlist backend.
159 */
160 private IPlaylistStrategy playlist;
161
162 /***
163 * Adds track to the playlist after given track
164 *
165 * @param index
166 * insert given track after track with this index.
167 *
168 * @param track
169 * the track to add.
170 */
171 public void add(int index, TrackMetadataBean track) {
172 add(index, Collections.singletonList(track));
173 }
174
175 public void add(int index, List<TrackMetadataBean> tracks) {
176 playlist.add(index, tracks);
177
178 getBusInvocator().playlistChanged(null);
179 }
180
181 private IPlaylistPlayerListener getBusInvocator() {
182 return app.getBus().getInvocator(IPlaylistPlayerListener.class, true);
183 }
184
185 /***
186 * Adds track to the end of the playlist.
187 *
188 * @param track
189 * the track to add.
190 */
191 public void add(TrackMetadataBean track) {
192 add(playlist.size(), track);
193 }
194
195 public void clearQueue() {
196 playlist.clearQueue();
197 getBusInvocator().playlistChanged(null);
198 }
199
200 public void dequeue(int track) {
201 playlist.dequeue(track);
202 }
203
204 public int getCurrentlyPlaying() {
205 return playlist.getCurrentlyPlaying();
206 }
207
208 /***
209 * Returns currently playing track.
210 *
211 * @return current track or <code>null</code> if no track is being played (
212 * {@link #getCurrentlyPlaying()} returns <code>-1</code>).
213 */
214 public TrackMetadataBean getCurrentlyPlayingTrack() {
215 if (playlist == null)
216 return null;
217 final int current = playlist.getCurrentlyPlaying();
218 return current < 0 ? null : playlist.getPlayItems().get(current)
219 .getTrack();
220 }
221
222 /***
223 * Returns currently playing track.
224 *
225 * @return current track or <code>null</code> if no track is being played (
226 * {@link #getCurrentlyPlaying()} returns <code>-1</code>).
227 */
228 public PlaylistItem getCurrentlyPlayingItem() {
229 final int current = playlist.getCurrentlyPlaying();
230 return current < 0 ? null : playlist.getPlayItems().get(current);
231 }
232
233 public List<PlaylistItem> getPlayItems() {
234 return playlist.getPlayItems();
235 }
236
237 public int next() {
238 final int track = playlist.next();
239 final boolean playing = getPlaybackState() != PlayerStateEnum.Stopped;
240 if (playing) {
241 psPlay(track, 0);
242 }
243 invokeTrackChanged(playing, 0);
244 return track;
245 }
246
247 public int play(int trackToPlay) {
248 return playWithSeek(trackToPlay, 0);
249 }
250
251 /***
252 * Plays given track. The playback continues from this track forward, unless
253 * random play is activated.
254 *
255 * @param trackToPlay
256 * the track to play. Index to the {@link #getPlayItems()} list.
257 * If <code>-1</code> then the current track pointer is moved
258 * before first track and the playback is stopped.
259 * @param startSeekMillis
260 * start playback here. Ignored when the stream is not seekable.
261 * @return the track that is being played.
262 */
263 public int playWithSeek(final int trackToPlay, final int startSeekMillis) {
264 final int track = playlist.play(trackToPlay);
265 psPlay(track, startSeekMillis);
266 invokeTrackChanged(true, startSeekMillis);
267 return track;
268 }
269
270 /***
271 * Plays current track from the beginning.
272 */
273 public void play() {
274 playWithSeek(0);
275 }
276
277 /***
278 * Plays current track from the beginning.
279 *
280 * @param startSeekMillis
281 * start playback here. Ignored when the stream is not seekable.
282 */
283 public void playWithSeek(final int startSeekMillis) {
284 playWithSeek(getCurrentlyPlaying(), startSeekMillis);
285 }
286
287 public int previous() {
288 final int track = playlist.previous();
289 final boolean playing = getPlaybackState() != PlayerStateEnum.Stopped;
290 if (playing) {
291 psPlay(track, 0);
292 }
293 invokeTrackChanged(playing, 0);
294 return track;
295 }
296
297 /***
298 * Requests the player service to play given track. The {@link #playlist} is not updated.
299 * @param track the track to play.
300 * @param startSeekMillis start seek time, in milliseconds.
301 */
302 void psPlay(int track, int startSeekMillis) {
303 if (track < 0) {
304 stop();
305 return;
306 }
307 final TrackMetadataBean bean = playlist.getPlayItems().get(track)
308 .getTrack();
309 try {
310 playerService.play(bean.getLocation(), bean.getOrigin().ordinal(),
311 startSeekMillis);
312 } catch (Exception e) {
313 throw new RuntimeException(e);
314 }
315 }
316
317 /***
318 * Queues given track, after all other queued tracks. Does nothing if the
319 * track is already queued.
320 *
321 * @param track
322 * the track to queue, index to the {@link #getPlayItems()} list.
323 */
324 public void queue(int track) {
325 queue(Interval.fromItem(track));
326 }
327
328 public void queue(final Interval tracks) {
329 if (tracks.isEmpty())
330 return;
331 playlist.queue(tracks);
332 getBusInvocator().playlistChanged(null);
333 }
334
335 public void reinit() {
336 playlist.reinit();
337 }
338
339 /***
340 * Removes a track with given index from the playlist.
341 *
342 * @param index
343 * the index.
344 * @return the removed track. If currently played track is removed then the
345 * playlist should move to play a next song.
346 */
347 public PlaylistItem remove(final int index) {
348 final PlaylistItem result = playlist.getPlayItems().get(index);
349 playlist.remove(Interval.fromItem(index));
350 getBusInvocator().playlistChanged(Interval.EMPTY);
351 return result;
352 }
353
354 /***
355 * Returns <code>true</code> if the playlist is a dynamic playlist.
356 *
357 * @return <code>true</code> if current playlist is dynamic.
358 */
359 public boolean isDynamic() {
360 return playlist instanceof DynamicPlaylistStrategy;
361 }
362
363 public void remove(Interval interval) {
364 if (interval.isEmpty())
365 return;
366 playlist.remove(interval);
367 getBusInvocator().playlistChanged(Interval.EMPTY);
368 }
369
370 public void setRandom(Random random) {
371 setRandomRepeat(random, repeat);
372 }
373
374 public Random getRandom() {
375 return random;
376 }
377
378 public void shuffle() {
379 playlist.shuffle();
380 getBusInvocator().playlistChanged(null);
381 }
382
383 public int size() {
384 return playlist.size();
385 }
386
387 public void sortByAlbumOrder() {
388 playlist.sortByAlbumOrder();
389 getBusInvocator().playlistChanged(null);
390 }
391
392 /***
393 * Changes the playlist backend to be a dynamic playlist. The playlist is
394 * populated automatically.
395 */
396 public void dynamicPlaylist() {
397 if (playlist instanceof DynamicPlaylistStrategy) {
398 return;
399 }
400
401 final IDynamicPlaylistTrackProvider provider = new MediaStoreTrackProvider(
402 Random.ALBUM);
403 playlist = new DynamicPlaylistStrategy(provider, playlist, app
404 .getStateHandler().getConfig().dynamicHistorySize, app
405 .getStateHandler().getConfig().dynamicUpcomingSize);
406 getBusInvocator().playlistChanged(null);
407 }
408
409 /***
410 * Changes the playlist backend to be an empty static playlist.
411 */
412 public void staticPlaylist() {
413 if (playlist instanceof StaticPlaylistStrategy)
414 return;
415 playlist = new StaticPlaylistStrategy(playlist);
416 }
417
418 /***
419 * Closes and deactivates the player. The instance is no more usable.
420 */
421 public void close() {
422 errHandler.close();
423 app.unbindService(serviceConnection);
424 app.getBus().removeHandler(listener);
425 }
426
427 private final Listener listener = new Listener();
428
429 private final Handler handler = AmbientApplication.getHandler();
430
431 private final class Listener implements IPlayerListener,
432 IApplicationListener, Runnable {
433
434 public void stateChanged(AppState state) {
435
436 }
437
438 public void started(String file, int duration, int currentPosition) {
439 errHandler.handleSuccess();
440 getBusInvocator().playbackStateChanged(PlayerStateEnum.Playing);
441 }
442
443 private boolean isQueued(int track) {
444 if (track < 0) {
445 return false;
446 }
447 return playlist.getPlayItems().get(track).getQueueOrder() > 0;
448 }
449
450 public void stopped(String error, boolean missing,
451 TrackOriginEnum origin) {
452
453 if (error != null) {
454 errHandler.handleError(error, missing, origin);
455 return;
456 }
457
458 if (repeat == Repeat.TRACK) {
459 playWithGap();
460 return;
461 }
462 final TrackMetadataBean prevTrack = getCurrentlyPlayingTrack();
463 int track = playlist.peekNext();
464 final boolean isNextQueued = isQueued(track);
465 if ((repeat == Repeat.ALBUM) && (prevTrack != null)
466 && !isNextQueued) {
467 if (isDynamic()) {
468
469 } else {
470
471 final TrackMetadataBean newTrack = track == -1 ? null
472 : playlist.getPlayItems().get(track).getTrack();
473 if ((newTrack == null)
474 || (!newTrack.getAlbum().equals(
475 prevTrack.getAlbum()))) {
476
477 final List<PlaylistItem> tracks = Utils.filterOnAlbum(
478 playlist.getPlayItems(), prevTrack.getAlbum());
479 if (!tracks.isEmpty()) {
480 Utils.sortPlaylistByAlbumOrder(tracks);
481 final PlaylistItem first = tracks.get(0);
482 final int firstIndex = playlist.getPlayItems()
483 .indexOf(first);
484 playWithGap(firstIndex);
485 return;
486 }
487 }
488 }
489
490 }
491 track = playlist.next();
492 if (track != -1) {
493 playWithGap();
494 return;
495 }
496
497 if ((repeat == Repeat.NO) || (playlist.size() == 0)) {
498
499 invokePlaybackStateChanged();
500 return;
501 }
502
503 if (next() != -1) {
504 playWithGap();
505 }
506 }
507
508 public void configChanged(ConfigurationBean config) {
509 if (isDynamic()) {
510 final DynamicPlaylistStrategy p = (DynamicPlaylistStrategy) playlist;
511 p.setHistoryLength(config.dynamicHistorySize);
512 p.setUpcomingTrackCount(config.dynamicUpcomingSize);
513 getBusInvocator().playlistChanged(null);
514 }
515 }
516
517 private void playWithGap() {
518 playWithGap(null);
519 }
520
521 private volatile Integer desiredTrack;
522
523 private void playWithGap(final Integer track) {
524 desiredTrack = track;
525 final long gap = app.getStateHandler().getConfig().playerGap;
526 if (gap == 0) {
527 run();
528 return;
529 }
530 handler.removeCallbacks(this);
531 handler.postDelayed(this, gap);
532 }
533
534 public void run() {
535 if (desiredTrack == null) {
536 play();
537 } else {
538 play(desiredTrack);
539 }
540 }
541
542 public void clipboardChanged() {
543
544 }
545
546 public void radioNewTrack(String name) {
547
548 }
549
550 public void buffered(byte percent) {
551
552 }
553
554 public void offline(boolean offline) {
555 if (offline) {
556 final TrackMetadataBean track = getCurrentlyPlayingTrack();
557 if ((track != null) && !track.isLocal()) {
558 stop();
559 }
560 }
561 }
562 }
563
564 /***
565 * The repeat mode.
566 */
567 private Repeat repeat = Repeat.NO;
568
569 /***
570 * Random mode as selected by user.
571 */
572 private Random random = Random.NONE;
573
574 private void setRandomRepeat(final Random random, final Repeat repeat) {
575 if (random == null)
576 throw new IllegalArgumentException("null random");
577 if (repeat == null)
578 throw new IllegalArgumentException("null repeat");
579
580
581
582 Random strategyRandom = random;
583 if (!isDynamic() && (repeat == Repeat.ALBUM)) {
584
585
586 if (random == Random.NONE) {
587 strategyRandom = Random.ALBUM_PLAYLIST;
588 } else if (random == Random.TRACK) {
589 strategyRandom = Random.ALBUM_TRACK;
590 }
591 }
592 playlist.setRandom(strategyRandom);
593 if (this.random != random) {
594 this.random = random;
595 getBusInvocator().randomChanged(random);
596 if (isDynamic()) {
597
598 getBusInvocator().playlistChanged(null);
599 }
600 }
601 if (this.repeat != repeat) {
602 this.repeat = repeat;
603 getBusInvocator().repeatChanged(repeat);
604 }
605 }
606
607 /***
608 * Sets the repeat mode.
609 *
610 * @param repeat
611 * repeat mode, must not be <code>null</code>.
612 */
613 public void setRepeat(final Repeat repeat) {
614 setRandomRepeat(random, repeat);
615 }
616
617 /***
618 * Returns the repeat mode.
619 *
620 * @return repeat mode, must not be <code>null</code>.
621 */
622 public Repeat getRepeat() {
623 return repeat;
624 }
625
626 private void invokePlaybackStateChanged() {
627 final PlayerStateEnum state = getPlaybackState();
628 getBusInvocator().playbackStateChanged(state);
629 }
630
631 private void invokeTrackChanged(final boolean play, final int positionMillis) {
632 final PlaylistItem item = getCurrentlyPlayingItem();
633 getBusInvocator().trackChanged(item, play, positionMillis);
634 if (isDynamic()) {
635 getBusInvocator().playlistChanged(null);
636 }
637 }
638
639 /***
640 * Returns current position in millis of the playback. Returns 0 if the
641 * playback is stopped.
642 *
643 * @return current player position in millis.
644 */
645 public int getPosition() {
646 try {
647 return playerService.getPosition();
648 } catch (Exception e) {
649 throw new RuntimeException(e);
650 }
651 }
652
653 public List<Integer> getQueue() {
654 return playlist.getQueue();
655 }
656
657 public Interval move(Interval interval, int targetIndex) {
658 if (interval.isEmpty() || interval.contains(targetIndex))
659 return interval;
660 final Interval result = playlist.move(interval, targetIndex);
661 getBusInvocator().playlistChanged(result);
662 return result;
663 }
664
665 /***
666 * Finds given track in the playlist and activates it. If the track is not
667 * found then track is deselected.
668 *
669 * @param track
670 * the track to activate, may be <code>null</code>.
671 */
672 private void setActive(final TrackMetadataBean track) {
673 if ((track == null) || isDynamic()) {
674 playlist.play(-1);
675 } else {
676 final List<PlaylistItem> items = playlist.getPlayItems();
677 for (int i = 0; i < items.size(); i++) {
678 final PlaylistItem item = items.get(i);
679 if (item.getTrack().equals(track)) {
680 playlist.play(i);
681 invokeTrackChanged(false, 0);
682 return;
683 }
684 }
685 playlist.play(-1);
686 }
687 invokeTrackChanged(false, 0);
688 }
689
690 /***
691 * Reinits the player with given playlist strategy.
692 *
693 * @param strategy
694 * the deserialized strategy
695 */
696 public void reinit(final IPlaylistStrategy strategy) {
697 TrackMetadataBean track = getCurrentlyPlayingTrack();
698 final Random prevRandom = getRandom();
699 playlist = strategy;
700 playlist.clearQueue();
701 playlist.setRandom(prevRandom);
702 setActive(track);
703 postReinit();
704 }
705
706 private void postReinit() {
707
708 if (isDynamic()) {
709 final DynamicPlaylistStrategy s = (DynamicPlaylistStrategy) playlist;
710 s
711 .setHistoryLength(app.getStateHandler().getConfig().dynamicHistorySize);
712 s
713 .setUpcomingTrackCount(app.getStateHandler().getConfig().dynamicUpcomingSize);
714 }
715 getBusInvocator().playlistChanged(null);
716 }
717
718 public void reinit(AppState state) {
719 playlist = state.playlist;
720 if (playlist == null) {
721 staticPlaylist();
722 }
723 setRandomRepeat(state.random != null ? state.random : Random.NONE,
724 state.repeat != null ? state.repeat : Repeat.NO);
725
726 stop();
727 if (state.state != PlayerStateEnum.Stopped) {
728 playWithSeek(state.position);
729 if (state.state == PlayerStateEnum.Paused) {
730 pause();
731 }
732 } else {
733 invokeTrackChanged(false, 0);
734 }
735 postReinit();
736 }
737
738 public void storeState(AppState result) {
739 final boolean store = app.getStateHandler().getConfig().rememberPlaylist;
740 result.playlist = store ? playlist : null;
741 result.position = store ? getPosition() : 0;
742 result.repeat = getRepeat();
743 result.random = getRandom();
744 result.state = store ? getPlaybackState() : PlayerStateEnum.Stopped;
745 }
746
747 public Interval moveBy(Interval interval, int delta) {
748 if (delta == 0)
749 return interval;
750 final Interval result = playlist.moveBy(interval, delta);
751 getBusInvocator().playlistChanged(result);
752 return result;
753 }
754
755 public int peekNext() {
756 return playlist.peekNext();
757 }
758
759 public void replaceLocations(Map<String, String> locationMap) {
760 playlist.replaceLocations(locationMap);
761 }
762 }