Skip to content

Commit 3803d49

Browse files
committed
Player/handleIntent: separate out the timestamp request into enum
Instead of implicitely reconstructing whether the intent was intended (lol) to be a timestamp change, we create a new kind of intent that *only* sets the data we need to switch to a new timestamp. This means that the logic of what to do (opening a popup player) gets moved from `InternalUrlsHandler.playOnPopup` to the `Player.handleIntent` method, we only pass that we want to jump to a new timestamp. Thus, the stream is now loaded *after* sending the intent instead of before sending. This is somewhat messy right now and still does not fix the issue of queue deletion, but from now on the queue logic should get more straightforward to implement. In the end, everything should be a giant switch. Thus we don’t fall-through anymore, but run the post-setup code manually by calling `handeIntentPost` and then returning.
1 parent 25a4a9a commit 3803d49

File tree

6 files changed

+121
-84
lines changed

6 files changed

+121
-84
lines changed

app/src/main/java/org/schabi/newpipe/player/Player.java

Lines changed: 80 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@
109109
import org.schabi.newpipe.player.playback.PlaybackListener;
110110
import org.schabi.newpipe.player.playqueue.PlayQueue;
111111
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
112+
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
112113
import org.schabi.newpipe.player.resolver.AudioPlaybackResolver;
113114
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
114115
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver.SourceType;
@@ -118,8 +119,10 @@
118119
import org.schabi.newpipe.player.ui.PopupPlayerUi;
119120
import org.schabi.newpipe.player.ui.VideoPlayerUi;
120121
import org.schabi.newpipe.util.DependentPreferenceHelper;
122+
import org.schabi.newpipe.util.ExtractorHelper;
121123
import org.schabi.newpipe.util.ListHelper;
122124
import org.schabi.newpipe.util.NavigationHelper;
125+
import org.schabi.newpipe.util.PermissionHelper;
123126
import org.schabi.newpipe.util.SerializedCache;
124127
import org.schabi.newpipe.util.StreamTypeUtil;
125128
import org.schabi.newpipe.util.image.PicassoHelper;
@@ -130,9 +133,11 @@
130133

131134
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
132135
import io.reactivex.rxjava3.core.Observable;
136+
import io.reactivex.rxjava3.core.Single;
133137
import io.reactivex.rxjava3.disposables.CompositeDisposable;
134138
import io.reactivex.rxjava3.disposables.Disposable;
135139
import io.reactivex.rxjava3.disposables.SerialDisposable;
140+
import io.reactivex.rxjava3.schedulers.Schedulers;
136141

137142
public final class Player implements PlaybackListener, Listener {
138143
public static final boolean DEBUG = MainActivity.DEBUG;
@@ -160,6 +165,7 @@ public final class Player implements PlaybackListener, Listener {
160165
public static final String PLAY_WHEN_READY = "play_when_ready";
161166
public static final String PLAYER_TYPE = "player_type";
162167
public static final String PLAYER_INTENT_TYPE = "player_intent_type";
168+
public static final String PLAYER_INTENT_DATA = "player_intent_data";
163169

164170
/*//////////////////////////////////////////////////////////////////////////
165171
// Time constants
@@ -244,6 +250,8 @@ public final class Player implements PlaybackListener, Listener {
244250
private final SerialDisposable progressUpdateDisposable = new SerialDisposable();
245251
@NonNull
246252
private final CompositeDisposable databaseUpdateDisposable = new CompositeDisposable();
253+
@NonNull
254+
private final CompositeDisposable streamItemDisposable = new CompositeDisposable();
247255

248256
// This is the only listener we need for thumbnail loading, since there is always at most only
249257
// one thumbnail being loaded at a time. This field is also here to maintain a strong reference,
@@ -344,48 +352,93 @@ public int getOverrideResolutionIndex(final List<VideoStream> sortedVideos,
344352

345353
@SuppressWarnings("MethodLength")
346354
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) {
355+
356+
final PlayerIntentType playerIntentType = intent.getParcelableExtra(PLAYER_INTENT_TYPE);
357+
if (playerIntentType == null) {
350358
return;
351359
}
352-
final PlayQueue newQueue = SerializedCache.getInstance().take(queueCache, PlayQueue.class);
353-
if (newQueue == null) {
354-
return;
360+
final PlayerType newPlayerType;
361+
// TODO: this should be in the second switch below, but I’m not sure whether I
362+
// can move the initUIs stuff without breaking the setup for edge cases somehow.
363+
switch (playerIntentType) {
364+
case TimestampChange -> {
365+
// TODO: this breaks out of the pattern of asking for the permission before
366+
// sending the PlayerIntent, but I’m not sure yet how to combine the permissions
367+
// with the new enum approach. Maybe it’s better that the player asks anyway?
368+
if (!PermissionHelper.isPopupEnabledElseAsk(context)) {
369+
return;
370+
}
371+
newPlayerType = PlayerType.POPUP;
372+
}
373+
default -> {
374+
newPlayerType = PlayerType.retrieveFromIntent(intent);
375+
}
355376
}
356377

357378
final PlayerType oldPlayerType = playerType;
358-
playerType = PlayerType.retrieveFromIntent(intent);
379+
playerType = newPlayerType;
359380
initUIsForCurrentPlayerType();
360381
isAudioOnly = audioPlayerSelected();
361382

362383
if (intent.hasExtra(PLAYBACK_QUALITY)) {
363384
videoResolver.setPlaybackQuality(intent.getStringExtra(PLAYBACK_QUALITY));
364385
}
365386

366-
final PlayerIntentType playerIntentType = intent.getParcelableExtra(PLAYER_INTENT_TYPE);
387+
final boolean playWhenReady = intent.getBooleanExtra(PLAY_WHEN_READY, true);
367388

368389
switch (playerIntentType) {
369390
case Enqueue -> {
370391
if (playQueue != null) {
392+
final PlayQueue newQueue = getPlayQueueFromCache(intent);
393+
if (newQueue == null) {
394+
return;
395+
}
371396
playQueue.append(newQueue.getStreams());
372397
}
373398
return;
374399
}
375400
case EnqueueNext -> {
376401
if (playQueue != null) {
402+
final PlayQueue newQueue = getPlayQueueFromCache(intent);
403+
if (newQueue == null) {
404+
return;
405+
}
377406
final int currentIndex = playQueue.getIndex();
378407
playQueue.append(newQueue.getStreams());
379408
playQueue.move(playQueue.size() - 1, currentIndex + 1);
380409
}
381410
return;
382411
}
412+
case TimestampChange -> {
413+
final TimestampChangeData dat = intent.getParcelableExtra(PLAYER_INTENT_DATA);
414+
assert dat != null;
415+
final Single<StreamInfo> single =
416+
ExtractorHelper.getStreamInfo(dat.getServiceId(), dat.getUrl(), false);
417+
streamItemDisposable.add(single.subscribeOn(Schedulers.io())
418+
.observeOn(AndroidSchedulers.mainThread())
419+
.subscribe(info -> {
420+
final PlayQueue newPlayQueue = new SinglePlayQueue(info,
421+
dat.getSeconds() * 1000L);
422+
NavigationHelper.playOnPopupPlayer(context, playQueue, false);
423+
}, throwable -> {
424+
// This will only show a snackbar if the passed context has a root view:
425+
// otherwise it will resort to showing a notification, so we are safe
426+
// here.
427+
ErrorUtil.createNotification(context,
428+
new ErrorInfo(throwable, UserAction.PLAY_ON_POPUP, dat.getUrl(),
429+
null, dat.getUrl()));
430+
}));
431+
return;
432+
}
383433
case AllOthers -> {
384434
// fallthrough; TODO: put other intent data in separate cases
385435
}
386436
}
387437

388-
final boolean playWhenReady = intent.getBooleanExtra(PLAY_WHEN_READY, true);
438+
final PlayQueue newQueue = getPlayQueueFromCache(intent);
439+
if (newQueue == null) {
440+
return;
441+
}
389442

390443
// branching parameters for below
391444
final boolean samePlayQueue = playQueue != null && playQueue.equalStreamsAndIndex(newQueue);
@@ -466,6 +519,10 @@ public void handleIntent(@NonNull final Intent intent) {
466519
initPlayback(samePlayQueue ? playQueue : newQueue, playWhenReady);
467520
}
468521

522+
handleIntentPost(oldPlayerType);
523+
}
524+
525+
private void handleIntentPost(final PlayerType oldPlayerType) {
469526
if (oldPlayerType != playerType && playQueue != null) {
470527
// If playerType changes from one to another we should reload the player
471528
// (to disable/enable video stream or to set quality)
@@ -476,6 +533,19 @@ public void handleIntent(@NonNull final Intent intent) {
476533
NavigationHelper.sendPlayerStartedEvent(context);
477534
}
478535

536+
@Nullable
537+
private static PlayQueue getPlayQueueFromCache(@NonNull final Intent intent) {
538+
final String queueCache = intent.getStringExtra(PLAY_QUEUE_KEY);
539+
if (queueCache == null) {
540+
return null;
541+
}
542+
final PlayQueue newQueue = SerializedCache.getInstance().take(queueCache, PlayQueue.class);
543+
if (newQueue == null) {
544+
return null;
545+
}
546+
return newQueue;
547+
}
548+
479549
private void initUIsForCurrentPlayerType() {
480550
if ((UIs.get(MainPlayerUi.class).isPresent() && playerType == PlayerType.MAIN)
481551
|| (UIs.get(PopupPlayerUi.class).isPresent() && playerType == PlayerType.POPUP)) {
@@ -605,6 +675,7 @@ public void destroy() {
605675

606676
databaseUpdateDisposable.clear();
607677
progressUpdateDisposable.set(null);
678+
streamItemDisposable.clear();
608679
cancelLoadingCurrentThumbnail();
609680

610681
UIs.destroyAll(Object.class); // destroy every UI: obviously every UI extends Object

app/src/main/java/org/schabi/newpipe/player/PlayerIntentType.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,16 @@ import kotlinx.parcelize.Parcelize
1111
enum class PlayerIntentType : Parcelable {
1212
Enqueue,
1313
EnqueueNext,
14+
TimestampChange,
1415
AllOthers
1516
}
17+
18+
/**
19+
* A timestamp on the given was clicked and we should switch the playing stream to it.
20+
*/
21+
@Parcelize
22+
data class TimestampChangeData(
23+
val serviceId: Int,
24+
val url: String,
25+
val seconds: Int
26+
) : Parcelable

app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
import org.schabi.newpipe.player.PlayerIntentType;
6262
import org.schabi.newpipe.player.PlayerService;
6363
import org.schabi.newpipe.player.PlayerType;
64+
import org.schabi.newpipe.player.TimestampChangeData;
6465
import org.schabi.newpipe.player.helper.PlayerHelper;
6566
import org.schabi.newpipe.player.helper.PlayerHolder;
6667
import org.schabi.newpipe.player.playqueue.PlayQueue;
@@ -103,6 +104,18 @@ public static <T> Intent getPlayerIntent(@NonNull final Context context,
103104
return intent;
104105
}
105106

107+
@NonNull
108+
public static Intent getPlayerTimestampIntent(@NonNull final Context context,
109+
@NonNull final TimestampChangeData
110+
timestampChangeData) {
111+
final Intent intent = new Intent(context, PlayerService.class);
112+
113+
intent.putExtra(Player.PLAYER_INTENT_TYPE, (Parcelable) PlayerIntentType.TimestampChange);
114+
intent.putExtra(Player.PLAYER_INTENT_DATA, timestampChangeData);
115+
116+
return intent;
117+
}
118+
106119
@NonNull
107120
public static <T> Intent getPlayerEnqueueNextIntent(@NonNull final Context context,
108121
@NonNull final Class<T> targetClazz,
Lines changed: 15 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package org.schabi.newpipe.util.text;
22

33
import android.content.Context;
4+
import android.content.Intent;
5+
import androidx.core.content.ContextCompat;
46

57
import androidx.annotation.NonNull;
68

@@ -13,19 +15,13 @@
1315
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
1416
import org.schabi.newpipe.extractor.exceptions.ParsingException;
1517
import org.schabi.newpipe.extractor.linkhandler.LinkHandlerFactory;
16-
import org.schabi.newpipe.extractor.stream.StreamInfo;
17-
import org.schabi.newpipe.player.playqueue.PlayQueue;
18-
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
19-
import org.schabi.newpipe.util.ExtractorHelper;
18+
import org.schabi.newpipe.player.TimestampChangeData;
2019
import org.schabi.newpipe.util.NavigationHelper;
2120

2221
import java.util.regex.Matcher;
2322
import java.util.regex.Pattern;
2423

25-
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
26-
import io.reactivex.rxjava3.core.Single;
2724
import io.reactivex.rxjava3.disposables.CompositeDisposable;
28-
import io.reactivex.rxjava3.schedulers.Schedulers;
2925

3026
public final class InternalUrlsHandler {
3127
private static final String TAG = InternalUrlsHandler.class.getSimpleName();
@@ -35,29 +31,6 @@ public final class InternalUrlsHandler {
3531
private static final Pattern HASHTAG_TIMESTAMP_PATTERN =
3632
Pattern.compile("(.*)#timestamp=(\\d+)");
3733

38-
private InternalUrlsHandler() {
39-
}
40-
41-
/**
42-
* Handle a YouTube timestamp comment URL in NewPipe.
43-
* <p>
44-
* This method will check if the provided url is a YouTube comment description URL ({@code
45-
* https://www.youtube.com/watch?v=}video_id{@code #timestamp=}time_in_seconds). If yes, the
46-
* popup player will be opened when the user will click on the timestamp in the comment,
47-
* at the time and for the video indicated in the timestamp.
48-
*
49-
* @param disposables a field of the Activity/Fragment class that calls this method
50-
* @param context the context to use
51-
* @param url the URL to check if it can be handled
52-
* @return true if the URL can be handled by NewPipe, false if it cannot
53-
*/
54-
public static boolean handleUrlCommentsTimestamp(@NonNull final CompositeDisposable
55-
disposables,
56-
final Context context,
57-
@NonNull final String url) {
58-
return handleUrl(context, url, HASHTAG_TIMESTAMP_PATTERN, disposables);
59-
}
60-
6134
/**
6235
* Handle a YouTube timestamp description URL in NewPipe.
6336
* <p>
@@ -71,31 +44,9 @@ public static boolean handleUrlCommentsTimestamp(@NonNull final CompositeDisposa
7144
* @param url the URL to check if it can be handled
7245
* @return true if the URL can be handled by NewPipe, false if it cannot
7346
*/
74-
public static boolean handleUrlDescriptionTimestamp(@NonNull final CompositeDisposable
75-
disposables,
76-
final Context context,
47+
public static boolean handleUrlDescriptionTimestamp(final Context context,
7748
@NonNull final String url) {
78-
return handleUrl(context, url, AMPERSAND_TIMESTAMP_PATTERN, disposables);
79-
}
80-
81-
/**
82-
* Handle an URL in NewPipe.
83-
* <p>
84-
* This method will check if the provided url can be handled in NewPipe or not. If this is a
85-
* service URL with a timestamp, the popup player will be opened and true will be returned;
86-
* else, false will be returned.
87-
*
88-
* @param context the context to use
89-
* @param url the URL to check if it can be handled
90-
* @param pattern the pattern to use
91-
* @param disposables a field of the Activity/Fragment class that calls this method
92-
* @return true if the URL can be handled by NewPipe, false if it cannot
93-
*/
94-
private static boolean handleUrl(final Context context,
95-
@NonNull final String url,
96-
@NonNull final Pattern pattern,
97-
@NonNull final CompositeDisposable disposables) {
98-
final Matcher matcher = pattern.matcher(url);
49+
final Matcher matcher = AMPERSAND_TIMESTAMP_PATTERN.matcher(url);
9950
if (!matcher.matches()) {
10051
return false;
10152
}
@@ -120,7 +71,7 @@ private static boolean handleUrl(final Context context,
12071
}
12172

12273
if (linkType == StreamingService.LinkType.STREAM && seconds != -1) {
123-
return playOnPopup(context, matchedUrl, service, seconds, disposables);
74+
return playOnPopup(context, matchedUrl, service, seconds);
12475
} else {
12576
NavigationHelper.openRouterActivity(context, matchedUrl);
12677
return true;
@@ -134,15 +85,12 @@ private static boolean handleUrl(final Context context,
13485
* @param url the URL of the content
13586
* @param service the service of the content
13687
* @param seconds the position in seconds at which the floating player will start
137-
* @param disposables disposables created by the method are added here and their lifecycle
138-
* should be handled by the calling class
13988
* @return true if the playback of the content has successfully started or false if not
14089
*/
14190
public static boolean playOnPopup(final Context context,
14291
final String url,
14392
@NonNull final StreamingService service,
144-
final int seconds,
145-
@NonNull final CompositeDisposable disposables) {
93+
final int seconds) {
14694
final LinkHandlerFactory factory = service.getStreamLHFactory();
14795
final String cleanUrl;
14896

@@ -152,19 +100,14 @@ public static boolean playOnPopup(final Context context,
152100
return false;
153101
}
154102

155-
final Single<StreamInfo> single =
156-
ExtractorHelper.getStreamInfo(service.getServiceId(), cleanUrl, false);
157-
disposables.add(single.subscribeOn(Schedulers.io())
158-
.observeOn(AndroidSchedulers.mainThread())
159-
.subscribe(info -> {
160-
final PlayQueue playQueue = new SinglePlayQueue(info, seconds * 1000L);
161-
NavigationHelper.playOnPopupPlayer(context, playQueue, false);
162-
}, throwable -> {
163-
// This will only show a snackbar if the passed context has a root view:
164-
// otherwise it will resort to showing a notification, so we are safe here.
165-
ErrorUtil.showSnackbar(context,
166-
new ErrorInfo(throwable, UserAction.PLAY_ON_POPUP, url, null, url));
167-
}));
103+
final Intent intent = NavigationHelper.getPlayerTimestampIntent(context,
104+
new TimestampChangeData(
105+
service.getServiceId(),
106+
cleanUrl,
107+
seconds
108+
));
109+
ContextCompat.startForegroundService(context, intent);
110+
168111
return true;
169112
}
170113
}

app/src/main/java/org/schabi/newpipe/util/text/TimestampLongPressClickableSpan.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ final class TimestampLongPressClickableSpan extends LongPressClickableSpan {
4646
@Override
4747
public void onClick(@NonNull final View view) {
4848
playOnPopup(context, relatedStreamUrl, relatedInfoService,
49-
timestampMatchDTO.seconds(), disposables);
49+
timestampMatchDTO.seconds());
5050
}
5151

5252
@Override

0 commit comments

Comments
 (0)