4646import static org .schabi .newpipe .util .ListHelper .getResolutionIndex ;
4747import static java .util .concurrent .TimeUnit .MILLISECONDS ;
4848
49+ import android .app .AlertDialog ;
4950import android .content .BroadcastReceiver ;
5051import android .content .Context ;
5152import android .content .Intent ;
8687import org .schabi .newpipe .R ;
8788import org .schabi .newpipe .databinding .PlayerBinding ;
8889import org .schabi .newpipe .error .ErrorInfo ;
90+ import org .schabi .newpipe .error .ErrorPanelHelper ;
8991import org .schabi .newpipe .error .ErrorUtil ;
9092import org .schabi .newpipe .error .UserAction ;
9193import org .schabi .newpipe .extractor .Image ;
109111import org .schabi .newpipe .player .playback .PlaybackListener ;
110112import org .schabi .newpipe .player .playqueue .PlayQueue ;
111113import org .schabi .newpipe .player .playqueue .PlayQueueItem ;
114+ import org .schabi .newpipe .player .playqueue .SinglePlayQueue ;
112115import org .schabi .newpipe .player .resolver .AudioPlaybackResolver ;
113116import org .schabi .newpipe .player .resolver .VideoPlaybackResolver ;
114117import org .schabi .newpipe .player .resolver .VideoPlaybackResolver .SourceType ;
118121import org .schabi .newpipe .player .ui .PopupPlayerUi ;
119122import org .schabi .newpipe .player .ui .VideoPlayerUi ;
120123import org .schabi .newpipe .util .DependentPreferenceHelper ;
124+ import org .schabi .newpipe .util .ExtractorHelper ;
121125import org .schabi .newpipe .util .ListHelper ;
122126import org .schabi .newpipe .util .NavigationHelper ;
127+ import org .schabi .newpipe .util .PermissionHelper ;
128+ import org .schabi .newpipe .util .image .PicassoHelper ;
123129import org .schabi .newpipe .util .SerializedCache ;
124130import org .schabi .newpipe .util .StreamTypeUtil ;
125- import org .schabi .newpipe .util .image .PicassoHelper ;
126131
127132import java .util .List ;
128133import java .util .Optional ;
129134import java .util .stream .IntStream ;
130135
131136import io .reactivex .rxjava3 .android .schedulers .AndroidSchedulers ;
132137import io .reactivex .rxjava3 .core .Observable ;
138+ import io .reactivex .rxjava3 .core .Single ;
133139import io .reactivex .rxjava3 .disposables .CompositeDisposable ;
134140import io .reactivex .rxjava3 .disposables .Disposable ;
135141import io .reactivex .rxjava3 .disposables .SerialDisposable ;
142+ import io .reactivex .rxjava3 .schedulers .Schedulers ;
136143
137144public final class Player implements PlaybackListener , Listener {
138145 public static final boolean DEBUG = MainActivity .DEBUG ;
@@ -160,6 +167,7 @@ public final class Player implements PlaybackListener, Listener {
160167 public static final String PLAY_WHEN_READY = "play_when_ready" ;
161168 public static final String PLAYER_TYPE = "player_type" ;
162169 public static final String PLAYER_INTENT_TYPE = "player_intent_type" ;
170+ public static final String PLAYER_INTENT_DATA = "player_intent_data" ;
163171
164172 /*//////////////////////////////////////////////////////////////////////////
165173 // Time constants
@@ -244,6 +252,8 @@ public final class Player implements PlaybackListener, Listener {
244252 private final SerialDisposable progressUpdateDisposable = new SerialDisposable ();
245253 @ NonNull
246254 private final CompositeDisposable databaseUpdateDisposable = new CompositeDisposable ();
255+ @ NonNull
256+ private final CompositeDisposable streamItemDisposable = new CompositeDisposable ();
247257
248258 // This is the only listener we need for thumbnail loading, since there is always at most only
249259 // one thumbnail being loaded at a time. This field is also here to maintain a strong reference,
@@ -344,18 +354,31 @@ public int getOverrideResolutionIndex(final List<VideoStream> sortedVideos,
344354
345355 @ SuppressWarnings ("MethodLength" )
346356 public void handleIntent (@ NonNull final Intent intent ) {
347- // fail fast if no play queue was provided
348- final String queueCache = intent .getStringExtra ( PLAY_QUEUE_KEY );
349- if (queueCache == null ) {
357+
358+ final PlayerIntentType playerIntentType = intent .getParcelableExtra ( PLAYER_INTENT_TYPE );
359+ if (playerIntentType == null ) {
350360 return ;
351361 }
352- final PlayQueue newQueue = SerializedCache .getInstance ().take (queueCache , PlayQueue .class );
353- if (newQueue == null ) {
354- return ;
362+ final PlayerType newPlayerType ;
363+ // TODO: this should be in the second switch below, but I’m not sure whether I
364+ // can move the initUIs stuff without breaking the setup for edge cases somehow.
365+ switch (playerIntentType ) {
366+ case TimestampChange -> {
367+ // TODO: this breaks out of the pattern of asking for the permission before
368+ // sending the PlayerIntent, but I’m not sure yet how to combine the permissions
369+ // with the new enum approach. Maybe it’s better that the player asks anyway?
370+ if (!PermissionHelper .isPopupEnabledElseAsk (context )) {
371+ return ;
372+ }
373+ newPlayerType = PlayerType .POPUP ;
374+ }
375+ default -> {
376+ newPlayerType = PlayerType .retrieveFromIntent (intent );
377+ }
355378 }
356379
357380 final PlayerType oldPlayerType = playerType ;
358- playerType = PlayerType . retrieveFromIntent ( intent ) ;
381+ playerType = newPlayerType ;
359382 initUIsForCurrentPlayerType ();
360383 // TODO: what does the following comment mean? Is that a relict?
361384 // We need to setup audioOnly before super(), see "sourceOf"
@@ -365,29 +388,61 @@ public void handleIntent(@NonNull final Intent intent) {
365388 videoResolver .setPlaybackQuality (intent .getStringExtra (PLAYBACK_QUALITY ));
366389 }
367390
368- final PlayerIntentType playerIntentType = intent .getParcelableExtra ( PLAYER_INTENT_TYPE );
391+ final boolean playWhenReady = intent .getBooleanExtra ( PLAY_WHEN_READY , true );
369392
370393 switch (playerIntentType ) {
371394 case Enqueue -> {
372395 if (playQueue != null ) {
396+ final PlayQueue newQueue = getPlayQueueFromCache (intent );
397+ if (newQueue == null ) {
398+ return ;
399+ }
373400 playQueue .append (newQueue .getStreams ());
374401 }
375402 return ;
376403 }
377404 case EnqueueNext -> {
378405 if (playQueue != null ) {
406+ final PlayQueue newQueue = getPlayQueueFromCache (intent );
407+ if (newQueue == null ) {
408+ return ;
409+ }
379410 final int currentIndex = playQueue .getIndex ();
380411 playQueue .append (newQueue .getStreams ());
381412 playQueue .move (playQueue .size () - 1 , currentIndex + 1 );
382413 }
383414 return ;
384415 }
416+ case TimestampChange -> {
417+ final TimestampChangeData dat = intent .getParcelableExtra (PLAYER_INTENT_DATA );
418+ assert dat != null ;
419+ final Single <StreamInfo > single =
420+ ExtractorHelper .getStreamInfo (dat .getServiceId (), dat .getUrl (), false );
421+ streamItemDisposable .add (single .subscribeOn (Schedulers .io ())
422+ .observeOn (AndroidSchedulers .mainThread ())
423+ .subscribe (info -> {
424+ final PlayQueue newPlayQueue = new SinglePlayQueue (info ,
425+ dat .getSeconds () * 1000L );
426+ NavigationHelper .playOnPopupPlayer (context , playQueue , false );
427+ }, throwable -> {
428+ final var errorInfo = new ErrorInfo (throwable , UserAction .PLAY_ON_POPUP ,
429+ dat .getUrl ());
430+ // This will only show a snackbar if the passed context has a root view:
431+ // otherwise it will resort to showing a notification, so we are safe
432+ // here.
433+ ErrorUtil .showSnackbar (context , errorInfo );
434+ }));
435+ return ;
436+ }
385437 case AllOthers -> {
386438 // fallthrough; TODO: put other intent data in separate cases
387439 }
388440 }
389441
390- final boolean playWhenReady = intent .getBooleanExtra (PLAY_WHEN_READY , true );
442+ final PlayQueue newQueue = getPlayQueueFromCache (intent );
443+ if (newQueue == null ) {
444+ return ;
445+ }
391446
392447 // branching parameters for below
393448 final boolean samePlayQueue = playQueue != null && playQueue .equalStreamsAndIndex (newQueue );
@@ -468,6 +523,10 @@ public void handleIntent(@NonNull final Intent intent) {
468523 initPlayback (samePlayQueue ? playQueue : newQueue , playWhenReady );
469524 }
470525
526+ handleIntentPost (oldPlayerType );
527+ }
528+
529+ private void handleIntentPost (final PlayerType oldPlayerType ) {
471530 if (oldPlayerType != playerType && playQueue != null ) {
472531 // If playerType changes from one to another we should reload the player
473532 // (to disable/enable video stream or to set quality)
@@ -478,6 +537,19 @@ public void handleIntent(@NonNull final Intent intent) {
478537 NavigationHelper .sendPlayerStartedEvent (context );
479538 }
480539
540+ @ Nullable
541+ private static PlayQueue getPlayQueueFromCache (@ NonNull final Intent intent ) {
542+ final String queueCache = intent .getStringExtra (PLAY_QUEUE_KEY );
543+ if (queueCache == null ) {
544+ return null ;
545+ }
546+ final PlayQueue newQueue = SerializedCache .getInstance ().take (queueCache , PlayQueue .class );
547+ if (newQueue == null ) {
548+ return null ;
549+ }
550+ return newQueue ;
551+ }
552+
481553 private void initUIsForCurrentPlayerType () {
482554 if ((UIs .get (MainPlayerUi .class ).isPresent () && playerType == PlayerType .MAIN )
483555 || (UIs .get (PopupPlayerUi .class ).isPresent () && playerType == PlayerType .POPUP )) {
@@ -607,6 +679,7 @@ public void destroy() {
607679
608680 databaseUpdateDisposable .clear ();
609681 progressUpdateDisposable .set (null );
682+ streamItemDisposable .clear ();
610683 cancelLoadingCurrentThumbnail ();
611684
612685 UIs .destroyAll (Object .class ); // destroy every UI: obviously every UI extends Object
0 commit comments