2525import java .io .Closeable ;
2626import java .io .File ;
2727import java .io .IOException ;
28+ import java .util .ArrayList ;
2829import java .util .List ;
2930import java .util .Map ;
30- import java .util .concurrent .Executors ;
31- import java .util .concurrent .ScheduledExecutorService ;
32- import java .util .concurrent .ScheduledFuture ;
33- import java .util .concurrent .TimeUnit ;
31+ import java .util .concurrent .*;
3432
3533import static spotify .player .proto .ContextTrackOuterClass .ContextTrack ;
3634
@@ -43,6 +41,7 @@ public class Player implements Closeable, DeviceStateHandler.Listener, PlayerRun
4341 private final Session session ;
4442 private final Configuration conf ;
4543 private final PlayerRunner runner ;
44+ private final EventsDispatcher events = new EventsDispatcher ();
4645 private StateWrapper state ;
4746 private TrackHandler trackHandler ;
4847 private TrackHandler crossfadeHandler ;
@@ -55,6 +54,14 @@ public Player(@NotNull Player.Configuration conf, @NotNull Session session) {
5554 new Thread (runner = new PlayerRunner (session , conf , this ), "player-runner-" + runner .hashCode ()).start ();
5655 }
5756
57+ public void addEventsListener (@ NotNull EventsListener listener ) {
58+ events .listeners .add (listener );
59+ }
60+
61+ public void removeEventsListener (@ NotNull EventsListener listener ) {
62+ events .listeners .remove (listener );
63+ }
64+
5865 public void initState () {
5966 this .state = new StateWrapper (session );
6067 state .addListener (this );
@@ -108,6 +115,8 @@ public void load(@NotNull String uri, boolean play) {
108115 }
109116
110117 loadTrack (play , PushToMixerReason .None );
118+ events .dispatchContextChanged ();
119+ events .dispatchTrackChanged ();
111120 }
112121
113122 private void transferState (TransferStateOuterClass .@ NotNull TransferState cmd ) {
@@ -126,6 +135,8 @@ private void transferState(TransferStateOuterClass.@NotNull TransferState cmd) {
126135 }
127136
128137 loadTrack (!cmd .getPlayback ().getIsPaused (), PushToMixerReason .None );
138+ events .dispatchContextChanged ();
139+ events .dispatchTrackChanged ();
129140 }
130141
131142 private void handleLoad (@ NotNull JsonObject obj ) {
@@ -146,6 +157,9 @@ private void handleLoad(@NotNull JsonObject obj) {
146157 Boolean play = PlayCommandHelper .isInitiallyPaused (obj );
147158 if (play == null ) play = true ;
148159 loadTrack (play , PushToMixerReason .None );
160+
161+ events .dispatchContextChanged ();
162+ events .dispatchTrackChanged ();
149163 }
150164
151165 @ Override
@@ -408,7 +422,12 @@ private void loadTrack(boolean play, @NotNull PushToMixerReason reason) {
408422 state .updated ();
409423 }
410424
411- if (!play ) runner .pauseMixer ();
425+ if (!play ) {
426+ runner .pauseMixer ();
427+ events .dispatchPlaybackPaused ();
428+ } else {
429+ events .dispatchPlaybackResumed ();
430+ }
412431 } else {
413432 if (preloadTrackHandler != null && preloadTrackHandler .isTrack (id )) {
414433 trackHandler = preloadTrackHandler ;
@@ -433,6 +452,9 @@ private void loadTrack(boolean play, @NotNull PushToMixerReason reason) {
433452 if (play ) {
434453 trackHandler .pushToMixer (reason );
435454 runner .playMixer ();
455+ events .dispatchPlaybackResumed ();
456+ } else {
457+ events .dispatchPlaybackPaused ();
436458 }
437459 }
438460
@@ -454,6 +476,7 @@ private void handleResume() {
454476 }
455477
456478 state .updated ();
479+ events .dispatchPlaybackResumed ();
457480
458481 if (releaseLineFuture != null ) {
459482 releaseLineFuture .cancel (true );
@@ -473,6 +496,7 @@ private void handlePause() {
473496 }
474497
475498 state .updated ();
499+ events .dispatchPlaybackPaused ();
476500
477501 if (releaseLineFuture != null ) releaseLineFuture .cancel (true );
478502 releaseLineFuture = scheduler .schedule (() -> {
@@ -507,6 +531,7 @@ private void handleNext(@Nullable JsonObject obj) {
507531 if (track != null ) {
508532 state .skipTo (track );
509533 loadTrack (true , PushToMixerReason .Next );
534+ events .dispatchTrackChanged ();
510535 return ;
511536 }
512537
@@ -519,6 +544,7 @@ private void handleNext(@Nullable JsonObject obj) {
519544 if (next .isOk ()) {
520545 state .setPosition (0 );
521546 loadTrack (next == NextPlayable .OK_PLAY || next == NextPlayable .OK_REPEAT , PushToMixerReason .Next );
547+ events .dispatchTrackChanged ();
522548 } else {
523549 LOGGER .fatal ("Failed loading next song: " + next );
524550 panicState ();
@@ -543,6 +569,8 @@ private void loadAutoplay() {
543569 state .setContextMetadata ("context_description" , contextDesc );
544570
545571 loadTrack (true , PushToMixerReason .None );
572+ events .dispatchContextChanged ();
573+ events .dispatchTrackChanged ();
546574
547575 LOGGER .debug (String .format ("Loading context for autoplay, uri: %s" , newContext ));
548576 } else if (resp .statusCode == 204 ) {
@@ -551,6 +579,8 @@ private void loadAutoplay() {
551579 state .setContextMetadata ("context_description" , contextDesc );
552580
553581 loadTrack (true , PushToMixerReason .None );
582+ events .dispatchContextChanged ();
583+ events .dispatchTrackChanged ();
554584
555585 LOGGER .debug (String .format ("Loading context for autoplay (using radio-apollo), uri: %s" , state .getContextUri ()));
556586 } else {
@@ -575,6 +605,7 @@ private void handlePrev() {
575605 if (prev .isOk ()) {
576606 state .setPosition (0 );
577607 loadTrack (true , PushToMixerReason .Prev );
608+ events .dispatchTrackChanged ();
578609 } else {
579610 LOGGER .fatal ("Failed loading previous song: " + prev );
580611 panicState ();
@@ -662,4 +693,55 @@ public interface Configuration {
662693
663694 int releaseLineDelay ();
664695 }
696+
697+ private interface EventsListener {
698+ void onContextChanged (@ NotNull String newUri );
699+
700+ void onTrackChanged (@ NotNull PlayableId id , @ Nullable Metadata .Track track , @ Nullable Metadata .Episode episode );
701+
702+ void onPlaybackPaused ();
703+
704+ void onPlaybackResumed ();
705+ }
706+
707+ private class EventsDispatcher {
708+ private final ExecutorService executorService = Executors .newSingleThreadExecutor (new NameThreadFactory ((r ) -> "player-events-" + r .hashCode ()));
709+ private final List <EventsListener > listeners = new ArrayList <>();
710+
711+ void dispatchPlaybackPaused () {
712+ for (EventsListener l : new ArrayList <>(listeners ))
713+ executorService .execute (l ::onPlaybackPaused );
714+ }
715+
716+ void dispatchPlaybackResumed () {
717+ for (EventsListener l : new ArrayList <>(listeners ))
718+ executorService .execute (l ::onPlaybackResumed );
719+ }
720+
721+ void dispatchContextChanged () {
722+ String uri = state .getContextUri ();
723+ if (uri == null ) return ;
724+
725+ for (EventsListener l : new ArrayList <>(listeners ))
726+ executorService .execute (() -> l .onContextChanged (uri ));
727+ }
728+
729+ void dispatchTrackChanged () {
730+ PlayableId id = state .getCurrentPlayable ();
731+ if (id == null ) return ;
732+
733+ Metadata .Track track ;
734+ Metadata .Episode episode ;
735+ if (trackHandler .isTrack (id )) {
736+ track = trackHandler .track ();
737+ episode = trackHandler .episode ();
738+ } else {
739+ track = null ;
740+ episode = null ;
741+ }
742+
743+ for (EventsListener l : new ArrayList <>(listeners ))
744+ executorService .execute (() -> l .onTrackChanged (id , track , episode ));
745+ }
746+ }
665747}
0 commit comments