From 14719572f5d40604f985ebd5e0ad6833a91f4ae9 Mon Sep 17 00:00:00 2001 From: Haggai Eran Date: Mon, 2 Jan 2023 21:43:28 +0200 Subject: [PATCH 01/31] Keep MediaSessionCompat and MediaSessionConnector in a separate class These objects need to live beyond the player for supporting MediaBrowserServiceCompat and Android Auto, so they need to move outside of the MediaSessionPlayerUi class. --- .../org/schabi/newpipe/player/Player.java | 2 +- .../schabi/newpipe/player/PlayerService.java | 46 +++++++++++++------ .../mediabrowser/MediaBrowserConnector.java | 32 +++++++++++++ .../mediasession/MediaSessionPlayerUi.java | 36 +++++++-------- 4 files changed, 81 insertions(+), 35 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index 920435a7e3b..dcf97785a21 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -302,7 +302,7 @@ public Player(@NonNull final PlayerService service) { // notification ui in the UIs list, since the notification depends on the media session in // PlayerUi#initPlayer(), and UIs.call() guarantees UI order is preserved. UIs = new PlayerUiList( - new MediaSessionPlayerUi(this), + new MediaSessionPlayerUi(this, service.getSessionConnector()), new NotificationPlayerUi(this) ); } diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayerService.java b/app/src/main/java/org/schabi/newpipe/player/PlayerService.java index e7abf4320d5..e460f608fad 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PlayerService.java +++ b/app/src/main/java/org/schabi/newpipe/player/PlayerService.java @@ -28,6 +28,9 @@ import android.os.IBinder; import android.util.Log; +import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector; + +import org.schabi.newpipe.player.mediabrowser.MediaBrowserConnector; import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi; import org.schabi.newpipe.player.notification.NotificationPlayerUi; import org.schabi.newpipe.util.ThemeHelper; @@ -47,6 +50,9 @@ public final class PlayerService extends Service { private final IBinder mBinder = new PlayerService.LocalBinder(this); + private MediaBrowserConnector mediaBrowserConnector; + + /*////////////////////////////////////////////////////////////////////////// // Service's LifeCycle //////////////////////////////////////////////////////////////////////////*/ @@ -59,15 +65,21 @@ public void onCreate() { assureCorrectAppLanguage(this); ThemeHelper.setTheme(this); - player = new Player(this); - /* - Create the player notification and start immediately the service in foreground, - otherwise if nothing is played or initializing the player and its components (especially - loading stream metadata) takes a lot of time, the app would crash on Android 8+ as the - service would never be put in the foreground while we said to the system we would do so - */ - player.UIs().get(NotificationPlayerUi.class) - .ifPresent(NotificationPlayerUi::createNotificationAndStartForeground); + mediaBrowserConnector = new MediaBrowserConnector(this); + } + + private void initializePlayer() { + if (player == null) { + player = new Player(this); + /* + Create the player notification and start immediately the service in foreground, + otherwise if nothing is played or initializing the player and its components (especially + loading stream metadata) takes a lot of time, the app would crash on Android 8+ as the + service would never be put in the foreground while we said to the system we would do so + */ + player.UIs().get(NotificationPlayerUi.class) + .ifPresent(NotificationPlayerUi::createNotificationAndStartForeground); + } } @Override @@ -104,11 +116,10 @@ public int onStartCommand(final Intent intent, final int flags, final int startI return START_NOT_STICKY; } - if (player != null) { - player.handleIntent(intent); - player.UIs().get(MediaSessionPlayerUi.class) - .ifPresent(ui -> ui.handleMediaButtonIntent(intent)); - } + initializePlayer(); + player.handleIntent(intent); + player.UIs().get(MediaSessionPlayerUi.class) + .ifPresent(ui -> ui.handleMediaButtonIntent(intent)); return START_NOT_STICKY; } @@ -143,6 +154,10 @@ public void onDestroy() { Log.d(TAG, "destroy() called"); } cleanup(); + if (mediaBrowserConnector != null) { + mediaBrowserConnector.release(); + mediaBrowserConnector = null; + } } private void cleanup() { @@ -167,6 +182,9 @@ public IBinder onBind(final Intent intent) { return mBinder; } + public MediaSessionConnector getSessionConnector() { + return mediaBrowserConnector.getSessionConnector(); + } public static class LocalBinder extends Binder { private final WeakReference playerService; diff --git a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java new file mode 100644 index 00000000000..6fc61c3d748 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java @@ -0,0 +1,32 @@ +package org.schabi.newpipe.player.mediabrowser; + +import android.support.v4.media.session.MediaSessionCompat; + +import androidx.annotation.NonNull; + +import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector; + +import org.schabi.newpipe.player.PlayerService; + +public class MediaBrowserConnector { + private static final String TAG = MediaBrowserConnector.class.getSimpleName(); + + private final PlayerService playerService; + private final @NonNull MediaSessionConnector sessionConnector; + private final @NonNull MediaSessionCompat mediaSession; + + public MediaBrowserConnector(@NonNull final PlayerService playerService) { + this.playerService = playerService; + mediaSession = new MediaSessionCompat(playerService, TAG); + sessionConnector = new MediaSessionConnector(mediaSession); + sessionConnector.setMetadataDeduplicationEnabled(true); + } + + public @NonNull MediaSessionConnector getSessionConnector() { + return sessionConnector; + } + + public void release() { + mediaSession.release(); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionPlayerUi.java index c673e688c47..8b69db82fa9 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionPlayerUi.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionPlayerUi.java @@ -50,8 +50,11 @@ public class MediaSessionPlayerUi extends PlayerUi private List prevNotificationActions = List.of(); - public MediaSessionPlayerUi(@NonNull final Player player) { + public MediaSessionPlayerUi(@NonNull final Player player, + @NonNull final MediaSessionConnector sessionConnector) { super(player); + this.mediaSession = sessionConnector.mediaSession; + this.sessionConnector = sessionConnector; ignoreHardwareMediaButtonsKey = context.getString(R.string.ignore_hardware_media_buttons_key); } @@ -61,10 +64,8 @@ public void initPlayer() { super.initPlayer(); destroyPlayer(); // release previously used resources - mediaSession = new MediaSessionCompat(context, TAG); mediaSession.setActive(true); - sessionConnector = new MediaSessionConnector(mediaSession); sessionConnector.setQueueNavigator(new PlayQueueNavigator(mediaSession, player)); sessionConnector.setPlayer(getForwardingPlayer()); @@ -77,7 +78,6 @@ public void initPlayer() { updateShouldIgnoreHardwareMediaButtons(player.getPrefs()); player.getPrefs().registerOnSharedPreferenceChangeListener(this); - sessionConnector.setMetadataDeduplicationEnabled(true); sessionConnector.setMediaMetadataProvider(exoPlayer -> buildMediaMetadata()); // force updating media session actions by resetting the previous ones @@ -89,27 +89,23 @@ public void initPlayer() { public void destroyPlayer() { super.destroyPlayer(); player.getPrefs().unregisterOnSharedPreferenceChangeListener(this); - if (sessionConnector != null) { - sessionConnector.setMediaButtonEventHandler(null); - sessionConnector.setPlayer(null); - sessionConnector.setQueueNavigator(null); - sessionConnector = null; - } - if (mediaSession != null) { - mediaSession.setActive(false); - mediaSession.release(); - mediaSession = null; - } + + sessionConnector.setMediaButtonEventHandler(null); + sessionConnector.setPlayer(null); + sessionConnector.setQueueNavigator(null); + sessionConnector.setMediaMetadataProvider(null); + + mediaSession.setActive(false); + prevNotificationActions = List.of(); } @Override public void onThumbnailLoaded(@Nullable final Bitmap bitmap) { super.onThumbnailLoaded(bitmap); - if (sessionConnector != null) { - // the thumbnail is now loaded: invalidate the metadata to trigger a metadata update - sessionConnector.invalidateMediaSessionMetadata(); - } + + // the thumbnail is now loaded: invalidate the metadata to trigger a metadata update + sessionConnector.invalidateMediaSessionMetadata(); } @@ -132,7 +128,7 @@ public void handleMediaButtonIntent(final Intent intent) { } public Optional getSessionToken() { - return Optional.ofNullable(mediaSession).map(MediaSessionCompat::getSessionToken); + return Optional.of(mediaSession.getSessionToken()); } From 31d7bfac61464faad1e92256dba79c18512db788 Mon Sep 17 00:00:00 2001 From: Haggai Eran Date: Sun, 25 Dec 2022 09:16:43 +0200 Subject: [PATCH 02/31] Simple playback status and controls in Android Auto Expose a MediaBrowserService from within the existing PlayerService, and use the existing MediaSession for Auto. Empty media browser for now. To test, one needs to enable "Unknown sources" in Android Auto's developer settings. Issue: #1758 --- app/src/main/AndroidManifest.xml | 3 ++ .../schabi/newpipe/player/PlayerService.java | 36 ++++++++++++++++- .../mediabrowser/MediaBrowserConnector.java | 39 +++++++++++++++++++ 3 files changed, 76 insertions(+), 2 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d11de9f478d..1950031cd09 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -64,6 +64,9 @@ + + + > result) { + result.detach(); + final var disposable = mediaBrowserConnector.onLoadChildren(parentId) + .subscribe(result::sendResult); + compositeDisposableLoadChildren.add(disposable); + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java index 6fc61c3d748..3c05b211983 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java @@ -1,13 +1,26 @@ package org.schabi.newpipe.player.mediabrowser; +import static org.schabi.newpipe.MainActivity.DEBUG; + +import android.os.Bundle; +import android.support.v4.media.MediaBrowserCompat; +import android.support.v4.media.MediaBrowserCompat.MediaItem; import android.support.v4.media.session.MediaSessionCompat; +import android.util.Log; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.media.MediaBrowserServiceCompat; import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector; import org.schabi.newpipe.player.PlayerService; +import java.util.ArrayList; +import java.util.List; + +import io.reactivex.rxjava3.core.Single; + public class MediaBrowserConnector { private static final String TAG = MediaBrowserConnector.class.getSimpleName(); @@ -20,6 +33,7 @@ public MediaBrowserConnector(@NonNull final PlayerService playerService) { mediaSession = new MediaSessionCompat(playerService, TAG); sessionConnector = new MediaSessionConnector(mediaSession); sessionConnector.setMetadataDeduplicationEnabled(true); + playerService.setSessionToken(mediaSession.getSessionToken()); } public @NonNull MediaSessionConnector getSessionConnector() { @@ -29,4 +43,29 @@ public MediaBrowserConnector(@NonNull final PlayerService playerService) { public void release() { mediaSession.release(); } + + @NonNull + private static final String MY_MEDIA_ROOT_ID = "media_root_id"; + + @Nullable + public MediaBrowserServiceCompat.BrowserRoot onGetRoot(@NonNull final String clientPackageName, + final int clientUid, + @Nullable final Bundle rootHints) { + if (DEBUG) { + Log.d(TAG, String.format("MediaBrowserService.onGetRoot(%s, %s, %s)", + clientPackageName, clientUid, rootHints)); + } + + return new MediaBrowserServiceCompat.BrowserRoot(MY_MEDIA_ROOT_ID, null); + } + + public Single> onLoadChildren(@NonNull final String parentId) { + if (DEBUG) { + Log.d(TAG, String.format("MediaBrowserService.onLoadChildren(%s)", parentId)); + } + + final List mediaItems = new ArrayList<>(); + + return Single.just(mediaItems); + } } From 90efadba52c058d4b8472383ef7f0fcce60da744 Mon Sep 17 00:00:00 2001 From: Haggai Eran Date: Sun, 25 Dec 2022 09:15:51 +0200 Subject: [PATCH 03/31] Manifest and metadata for Android Auto Add icon for Auto to manifest, and describe NewPipe as a media app. --- app/src/main/AndroidManifest.xml | 5 +++++ app/src/main/res/xml/automotive_app_desc.xml | 3 +++ 2 files changed, 8 insertions(+) create mode 100644 app/src/main/res/xml/automotive_app_desc.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1950031cd09..9683ffcc485 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -427,5 +427,10 @@ + + + diff --git a/app/src/main/res/xml/automotive_app_desc.xml b/app/src/main/res/xml/automotive_app_desc.xml new file mode 100644 index 00000000000..90e6f30efe6 --- /dev/null +++ b/app/src/main/res/xml/automotive_app_desc.xml @@ -0,0 +1,3 @@ + + + From 6822362bd6f7f25761f9646359bbada85b5fe8e8 Mon Sep 17 00:00:00 2001 From: Haggai Eran Date: Sat, 4 Feb 2023 18:05:28 +0200 Subject: [PATCH 04/31] player: seek to new index when given a new playqueue with a different index This happens in the MediaBrowserServiceCompat flow (playing a playlist). --- app/src/main/java/org/schabi/newpipe/player/Player.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index dcf97785a21..7f5710b7e9a 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -415,6 +415,10 @@ public void handleIntent(@NonNull final Intent intent) { == com.google.android.exoplayer2.Player.STATE_IDLE) { simpleExoPlayer.prepare(); } + if (playQueue.getIndex() != newQueue.getIndex()) { + simpleExoPlayer.seekTo(newQueue.getIndex(), + newQueue.getItem().getRecoveryPosition()); + } simpleExoPlayer.setPlayWhenReady(playWhenReady); } else if (intent.getBooleanExtra(RESUME_PLAYBACK, false) From 485635adf8bbde36f489cfe591889d6d911aec66 Mon Sep 17 00:00:00 2001 From: Haggai Eran Date: Sun, 15 Jan 2023 01:11:34 +0200 Subject: [PATCH 05/31] Media browser interface to show playlists on Android Auto --- .../mediabrowser/MediaBrowserConnector.java | 217 +++++++++++++++++- 1 file changed, 212 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java index 3c05b211983..b057b3f3507 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java @@ -2,37 +2,60 @@ import static org.schabi.newpipe.MainActivity.DEBUG; +import android.net.Uri; import android.os.Bundle; -import android.support.v4.media.MediaBrowserCompat; +import android.os.ResultReceiver; import android.support.v4.media.MediaBrowserCompat.MediaItem; +import android.support.v4.media.MediaDescriptionCompat; import android.support.v4.media.session.MediaSessionCompat; +import android.support.v4.media.session.PlaybackStateCompat; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.StringRes; import androidx.media.MediaBrowserServiceCompat; +import androidx.media.utils.MediaConstants; +import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector; +import org.schabi.newpipe.NewPipeDatabase; +import org.schabi.newpipe.R; +import org.schabi.newpipe.database.AppDatabase; +import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; +import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; +import org.schabi.newpipe.local.playlist.LocalPlaylistManager; import org.schabi.newpipe.player.PlayerService; +import org.schabi.newpipe.player.playqueue.PlayQueue; +import org.schabi.newpipe.player.playqueue.SinglePlayQueue; +import org.schabi.newpipe.util.NavigationHelper; import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.disposables.Disposable; -public class MediaBrowserConnector { +public class MediaBrowserConnector implements MediaSessionConnector.PlaybackPreparer { private static final String TAG = MediaBrowserConnector.class.getSimpleName(); private final PlayerService playerService; private final @NonNull MediaSessionConnector sessionConnector; private final @NonNull MediaSessionCompat mediaSession; + private AppDatabase database; + private LocalPlaylistManager localPlaylistManager; + private Disposable prepareOrPlayDisposable; + public MediaBrowserConnector(@NonNull final PlayerService playerService) { this.playerService = playerService; mediaSession = new MediaSessionCompat(playerService, TAG); sessionConnector = new MediaSessionConnector(mediaSession); sessionConnector.setMetadataDeduplicationEnabled(true); + sessionConnector.setPlaybackPreparer(this); playerService.setSessionToken(mediaSession.getSessionToken()); } @@ -41,11 +64,58 @@ public MediaBrowserConnector(@NonNull final PlayerService playerService) { } public void release() { + disposePrepareOrPlayCommands(); mediaSession.release(); } @NonNull - private static final String MY_MEDIA_ROOT_ID = "media_root_id"; + private static final String ID_ROOT = "//${BuildConfig.APPLICATION_ID}/r"; + @NonNull + private static final String ID_BOOKMARKS = ID_ROOT + "/playlists"; + + private MediaItem createRootMediaItem(final String mediaId, final String folderName) { + final var builder = new MediaDescriptionCompat.Builder(); + builder.setMediaId(mediaId); + builder.setTitle(folderName); + + final var extras = new Bundle(); + extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, + playerService.getString(R.string.app_name)); + builder.setExtras(extras); + return new MediaItem(builder.build(), MediaItem.FLAG_BROWSABLE); + } + + private MediaItem createPlaylistMediaItem(final PlaylistMetadataEntry playlist) { + final var builder = new MediaDescriptionCompat.Builder(); + builder.setMediaId(createMediaIdForPlaylist(playlist.getUid())) + .setTitle(playlist.name) + .setIconUri(Uri.parse(playlist.thumbnailUrl)); + + final var extras = new Bundle(); + extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, + playerService.getResources().getString(R.string.tab_bookmarks)); + builder.setExtras(extras); + return new MediaItem(builder.build(), MediaItem.FLAG_BROWSABLE); + } + + private String createMediaIdForPlaylist(final long playlistId) { + return ID_BOOKMARKS + '/' + playlistId; + } + + private MediaItem createPlaylistStreamMediaItem(final long playlistId, + final PlaylistStreamEntry item, + final int index) { + final var builder = new MediaDescriptionCompat.Builder(); + builder.setMediaId(createMediaIdForPlaylistIndex(playlistId, index)) + .setTitle(item.getStreamEntity().getTitle()) + .setIconUri(Uri.parse(item.getStreamEntity().getThumbnailUrl())); + + return new MediaItem(builder.build(), MediaItem.FLAG_PLAYABLE); + } + + private String createMediaIdForPlaylistIndex(final long playlistId, final int index) { + return createMediaIdForPlaylist(playlistId) + '/' + index; + } @Nullable public MediaBrowserServiceCompat.BrowserRoot onGetRoot(@NonNull final String clientPackageName, @@ -56,7 +126,7 @@ public MediaBrowserServiceCompat.BrowserRoot onGetRoot(@NonNull final String cli clientPackageName, clientUid, rootHints)); } - return new MediaBrowserServiceCompat.BrowserRoot(MY_MEDIA_ROOT_ID, null); + return new MediaBrowserServiceCompat.BrowserRoot(ID_ROOT, null); } public Single> onLoadChildren(@NonNull final String parentId) { @@ -64,8 +134,145 @@ public Single> onLoadChildren(@NonNull final String parentId) { Log.d(TAG, String.format("MediaBrowserService.onLoadChildren(%s)", parentId)); } - final List mediaItems = new ArrayList<>(); + final List mediaItems = new ArrayList<>(); + final var parentIdUri = Uri.parse(parentId); + if (parentId.equals(ID_ROOT)) { + mediaItems.add( + createRootMediaItem(ID_BOOKMARKS, + playerService.getResources().getString(R.string.tab_bookmarks))); + + } else if (parentId.startsWith(ID_BOOKMARKS)) { + final var path = parentIdUri.getPathSegments(); + if (path.size() == 2) { + return populateBookmarks(); + } else if (path.size() == 3) { + final var playlistId = Long.parseLong(path.get(2)); + return populatePlaylist(playlistId); + } else { + Log.w(TAG, "Unknown playlist uri " + parentId); + } + } return Single.just(mediaItems); } + + private LocalPlaylistManager getPlaylistManager() { + if (database == null) { + database = NewPipeDatabase.getInstance(playerService); + } + if (localPlaylistManager == null) { + localPlaylistManager = new LocalPlaylistManager(database); + } + return localPlaylistManager; + } + + private Single> populateBookmarks() { + final var playlists = getPlaylistManager().getPlaylists().firstOrError(); + return playlists.map(playlist -> + playlist.stream().map(this::createPlaylistMediaItem).collect(Collectors.toList())); + } + + private Single> populatePlaylist(final long playlistId) { + final var playlist = getPlaylistManager().getPlaylistStreams(playlistId).firstOrError(); + return playlist.map(items -> { + final List results = new ArrayList<>(); + int index = 0; + for (final var item : items) { + results.add(createPlaylistStreamMediaItem(playlistId, item, index)); + ++index; + } + return results; + }); + } + + private void playbackError(@StringRes final int resId, final int code) { + playerService.stopForImmediateReusing(); + sessionConnector.setCustomErrorMessage(playerService.getString(resId), code); + } + + private Single extractPlayQueueFromMediaId(final String mediaId) { + final Uri mediaIdUri = Uri.parse(mediaId); + if (mediaIdUri == null) { + return Single.error(new NullPointerException()); + } + if (mediaId.startsWith(ID_BOOKMARKS)) { + final var path = mediaIdUri.getPathSegments(); + if (path.size() == 4) { + final long playlistId = Long.parseLong(path.get(2)); + final int index = Integer.parseInt(path.get(3)); + + return getPlaylistManager() + .getPlaylistStreams(playlistId) + .firstOrError() + .map(items -> { + final var infoItems = items.stream() + .map(PlaylistStreamEntry::toStreamInfoItem) + .collect(Collectors.toList()); + return new SinglePlayQueue(infoItems, index); + }); + } + } + + return Single.error(new NullPointerException()); + } + + @Override + public long getSupportedPrepareActions() { + return PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID; + } + + private void disposePrepareOrPlayCommands() { + if (prepareOrPlayDisposable != null) { + prepareOrPlayDisposable.dispose(); + prepareOrPlayDisposable = null; + } + } + + @Override + public void onPrepare(final boolean playWhenReady) { + disposePrepareOrPlayCommands(); + // No need to prepare + } + + @Override + public void onPrepareFromMediaId(@NonNull final String mediaId, final boolean playWhenReady, + @Nullable final Bundle extras) { + if (DEBUG) { + Log.d(TAG, String.format("MediaBrowserConnector.onPrepareFromMediaId(%s, %s, %s)", + mediaId, playWhenReady, extras)); + } + + disposePrepareOrPlayCommands(); + prepareOrPlayDisposable = extractPlayQueueFromMediaId(mediaId) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + playQueue -> { + sessionConnector.setCustomErrorMessage(null); + NavigationHelper.playOnBackgroundPlayer(playerService, playQueue, + playWhenReady); + }, + throwable -> playbackError(R.string.error_http_not_found, + PlaybackStateCompat.ERROR_CODE_NOT_SUPPORTED) + ); + } + + @Override + public void onPrepareFromSearch(@NonNull final String query, final boolean playWhenReady, + @Nullable final Bundle extras) { + disposePrepareOrPlayCommands(); + playbackError(R.string.content_not_supported, PlaybackStateCompat.ERROR_CODE_NOT_SUPPORTED); + } + + @Override + public void onPrepareFromUri(@NonNull final Uri uri, final boolean playWhenReady, + @Nullable final Bundle extras) { + disposePrepareOrPlayCommands(); + playbackError(R.string.content_not_supported, PlaybackStateCompat.ERROR_CODE_NOT_SUPPORTED); + } + + @Override + public boolean onCommand(@NonNull final Player player, @NonNull final String command, + @Nullable final Bundle extras, @Nullable final ResultReceiver cb) { + return false; + } } From c1d24adb47fa27105793059184ae18440484ea2a Mon Sep 17 00:00:00 2001 From: Haggai Eran Date: Sat, 4 Feb 2023 17:53:28 +0200 Subject: [PATCH 06/31] StreamHistoryEntry: convert to StreamInfoItem Can be used to play history items. --- .../database/history/model/StreamHistoryEntry.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntry.kt b/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntry.kt index a93ba1652f6..7bc0592490f 100644 --- a/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntry.kt +++ b/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntry.kt @@ -3,6 +3,8 @@ package org.schabi.newpipe.database.history.model import androidx.room.ColumnInfo import androidx.room.Embedded import org.schabi.newpipe.database.stream.model.StreamEntity +import org.schabi.newpipe.extractor.stream.StreamInfoItem +import org.schabi.newpipe.util.image.ImageStrategy import java.time.OffsetDateTime data class StreamHistoryEntry( @@ -27,4 +29,14 @@ data class StreamHistoryEntry( return this.streamEntity.uid == other.streamEntity.uid && streamId == other.streamId && accessDate.isEqual(other.accessDate) } + + fun toStreamInfoItem(): StreamInfoItem { + val item = StreamInfoItem(streamEntity.serviceId, streamEntity.url, streamEntity.title, streamEntity.streamType) + item.duration = streamEntity.duration + item.uploaderName = streamEntity.uploader + item.uploaderUrl = streamEntity.uploaderUrl + item.thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl) + + return item + } } From 97a0e303cd91ba922d19c54300627930502855dd Mon Sep 17 00:00:00 2001 From: Haggai Eran Date: Sat, 4 Feb 2023 18:09:52 +0200 Subject: [PATCH 07/31] MediaBrowser: expose search history --- .../mediabrowser/MediaBrowserConnector.java | 49 +++++++++++++++++-- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java index b057b3f3507..083aafa57dc 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java @@ -23,6 +23,7 @@ import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; import org.schabi.newpipe.database.AppDatabase; +import org.schabi.newpipe.database.history.model.StreamHistoryEntry; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; import org.schabi.newpipe.local.playlist.LocalPlaylistManager; @@ -72,6 +73,10 @@ public void release() { private static final String ID_ROOT = "//${BuildConfig.APPLICATION_ID}/r"; @NonNull private static final String ID_BOOKMARKS = ID_ROOT + "/playlists"; + @NonNull + private static final String ID_HISTORY = ID_ROOT + "/history"; + @NonNull + private static final String ID_STREAM = ID_ROOT + "/stream"; private MediaItem createRootMediaItem(final String mediaId, final String folderName) { final var builder = new MediaDescriptionCompat.Builder(); @@ -141,7 +146,9 @@ public Single> onLoadChildren(@NonNull final String parentId) { mediaItems.add( createRootMediaItem(ID_BOOKMARKS, playerService.getResources().getString(R.string.tab_bookmarks))); - + mediaItems.add( + createRootMediaItem(ID_HISTORY, + playerService.getResources().getString(R.string.action_history))); } else if (parentId.startsWith(ID_BOOKMARKS)) { final var path = parentIdUri.getPathSegments(); if (path.size() == 2) { @@ -152,16 +159,38 @@ public Single> onLoadChildren(@NonNull final String parentId) { } else { Log.w(TAG, "Unknown playlist uri " + parentId); } + } else if (parentId.equals(ID_HISTORY)) { + return populateHistory(); } return Single.just(mediaItems); } - private LocalPlaylistManager getPlaylistManager() { + private Single> populateHistory() { + final var streamHistory = getDatabase().streamHistoryDAO(); + final var history = streamHistory.getHistory().firstOrError(); + return history.map(items -> + items.stream().map(this::createHistoryMediaItem).collect(Collectors.toList())); + } + + private MediaItem createHistoryMediaItem(@NonNull final StreamHistoryEntry streamHistoryEntry) { + final var builder = new MediaDescriptionCompat.Builder(); + builder.setMediaId(ID_STREAM + '/' + streamHistoryEntry.getStreamId()) + .setTitle(streamHistoryEntry.getStreamEntity().getTitle()) + .setIconUri(Uri.parse(streamHistoryEntry.getStreamEntity().getThumbnailUrl())); + + return new MediaItem(builder.build(), MediaItem.FLAG_PLAYABLE); + } + + private AppDatabase getDatabase() { if (database == null) { database = NewPipeDatabase.getInstance(playerService); } + return database; + } + + private LocalPlaylistManager getPlaylistManager() { if (localPlaylistManager == null) { - localPlaylistManager = new LocalPlaylistManager(database); + localPlaylistManager = new LocalPlaylistManager(getDatabase()); } return localPlaylistManager; } @@ -211,6 +240,20 @@ private Single extractPlayQueueFromMediaId(final String mediaId) { return new SinglePlayQueue(infoItems, index); }); } + } else if (mediaId.startsWith(ID_STREAM)) { + final var path = mediaIdUri.getPathSegments(); + if (path.size() == 3) { + final long streamId = Long.parseLong(path.get(2)); + return getDatabase().streamHistoryDAO().getHistory() + .firstOrError() + .map(items -> { + final var infoItems = items.stream() + .filter(it -> it.getStreamId() == streamId) + .map(StreamHistoryEntry::toStreamInfoItem) + .collect(Collectors.toList()); + return new SinglePlayQueue(infoItems, 0); + }); + } } return Single.error(new NullPointerException()); From 4f8cca7d0473c6667662ac3bf7b161bfd8f9e96d Mon Sep 17 00:00:00 2001 From: Haggai Eran Date: Fri, 2 Jun 2023 11:46:12 +0300 Subject: [PATCH 08/31] Pass media browser error as ErrorInfo --- .../mediabrowser/MediaBrowserConnector.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java index 083aafa57dc..df5b6d1db38 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java @@ -26,6 +26,9 @@ import org.schabi.newpipe.database.history.model.StreamHistoryEntry; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; +import org.schabi.newpipe.error.ErrorInfo; +import org.schabi.newpipe.error.UserAction; +import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; import org.schabi.newpipe.local.playlist.LocalPlaylistManager; import org.schabi.newpipe.player.PlayerService; import org.schabi.newpipe.player.playqueue.PlayQueue; @@ -219,10 +222,14 @@ private void playbackError(@StringRes final int resId, final int code) { sessionConnector.setCustomErrorMessage(playerService.getString(resId), code); } + private void playbackError(@NonNull final ErrorInfo errorInfo) { + playbackError(errorInfo.getMessageStringId(), PlaybackStateCompat.ERROR_CODE_APP_ERROR); + } + private Single extractPlayQueueFromMediaId(final String mediaId) { final Uri mediaIdUri = Uri.parse(mediaId); if (mediaIdUri == null) { - return Single.error(new NullPointerException()); + return Single.error(new ContentNotAvailableException("Media ID cannot be parsed")); } if (mediaId.startsWith(ID_BOOKMARKS)) { final var path = mediaIdUri.getPathSegments(); @@ -256,7 +263,7 @@ private Single extractPlayQueueFromMediaId(final String mediaId) { } } - return Single.error(new NullPointerException()); + return Single.error(new ContentNotAvailableException("Media ID cannot be parsed")); } @Override @@ -294,8 +301,8 @@ public void onPrepareFromMediaId(@NonNull final String mediaId, final boolean pl NavigationHelper.playOnBackgroundPlayer(playerService, playQueue, playWhenReady); }, - throwable -> playbackError(R.string.error_http_not_found, - PlaybackStateCompat.ERROR_CODE_NOT_SUPPORTED) + throwable -> playbackError(new ErrorInfo(throwable, UserAction.PLAY_STREAM, + "Failed playback of media ID [" + mediaId + "]: ")) ); } From b3226f7dfead78d0cdc012b1b4901c8fb2273f24 Mon Sep 17 00:00:00 2001 From: AudricV <74829229+AudricV@users.noreply.github.com> Date: Fri, 11 Aug 2023 13:22:43 +0200 Subject: [PATCH 09/31] Improve code formatting, annotate more fields and methods Also simplify logic in MediaBrowserConnector.extractPlayQueueFromMediaId, suppress some Sonar warnings as they could not be solved, and use explicit types in some variables. --- app/src/main/AndroidManifest.xml | 2 +- .../history/model/StreamHistoryEntry.kt | 7 +- .../schabi/newpipe/player/PlayerService.java | 56 +++++--- .../mediabrowser/MediaBrowserConnector.java | 131 ++++++++++-------- 4 files changed, 117 insertions(+), 79 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9683ffcc485..e52dded5e1a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -431,6 +431,6 @@ + android:resource="@mipmap/ic_launcher" /> diff --git a/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntry.kt b/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntry.kt index 7bc0592490f..1eb299890e6 100644 --- a/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntry.kt +++ b/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntry.kt @@ -31,7 +31,12 @@ data class StreamHistoryEntry( } fun toStreamInfoItem(): StreamInfoItem { - val item = StreamInfoItem(streamEntity.serviceId, streamEntity.url, streamEntity.title, streamEntity.streamType) + val item = StreamInfoItem( + streamEntity.serviceId, + streamEntity.url, + streamEntity.title, + streamEntity.streamType + ) item.duration = streamEntity.duration item.uploaderName = streamEntity.uploader item.uploaderUrl = streamEntity.uploaderUrl diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayerService.java b/app/src/main/java/org/schabi/newpipe/player/PlayerService.java index c070c0d88c9..55fdfd3a8ac 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PlayerService.java +++ b/app/src/main/java/org/schabi/newpipe/player/PlayerService.java @@ -43,8 +43,10 @@ import java.lang.ref.WeakReference; import java.util.List; +import java.util.Objects; import io.reactivex.rxjava3.disposables.CompositeDisposable; +import io.reactivex.rxjava3.disposables.Disposable; /** * One service for all players. @@ -53,6 +55,7 @@ public final class PlayerService extends MediaBrowserServiceCompat { private static final String TAG = PlayerService.class.getSimpleName(); private static final boolean DEBUG = Player.DEBUG; + @Nullable private Player player; private final IBinder mBinder = new PlayerService.LocalBinder(this); @@ -79,7 +82,7 @@ public void onCreate() { mediaBrowserConnector = new MediaBrowserConnector(this); } - private void initializePlayer() { + private void initializePlayerIfNeeded() { if (player == null) { player = new Player(this); /* @@ -93,6 +96,9 @@ otherwise if nothing is played or initializing the player and its components (es } } + // Suppress Sonar warning to not always return the same value, as we need to do some actions + // before returning + @SuppressWarnings("squid:S3516") @Override public int onStartCommand(final Intent intent, final int flags, final int startId) { if (DEBUG) { @@ -127,8 +133,8 @@ public int onStartCommand(final Intent intent, final int flags, final int startI return START_NOT_STICKY; } - initializePlayer(); - player.handleIntent(intent); + initializePlayerIfNeeded(); + Objects.requireNonNull(player).handleIntent(intent); player.UIs().get(MediaSessionPlayerUi.class) .ifPresent(ui -> ui.handleMediaButtonIntent(intent)); @@ -164,11 +170,14 @@ public void onDestroy() { if (DEBUG) { Log.d(TAG, "destroy() called"); } + cleanup(); + if (mediaBrowserConnector != null) { mediaBrowserConnector.release(); mediaBrowserConnector = null; } + compositeDisposableLoadChildren.clear(); } @@ -190,36 +199,23 @@ protected void attachBaseContext(final Context base) { } @Override - public IBinder onBind(final Intent intent) { + public IBinder onBind(@NonNull final Intent intent) { if (SERVICE_INTERFACE.equals(intent.getAction())) { return super.onBind(intent); } return mBinder; } + @NonNull public MediaSessionConnector getSessionConnector() { return mediaBrowserConnector.getSessionConnector(); } - public static class LocalBinder extends Binder { - private final WeakReference playerService; - - LocalBinder(final PlayerService playerService) { - this.playerService = new WeakReference<>(playerService); - } - - public PlayerService getService() { - return playerService.get(); - } - - public Player getPlayer() { - return playerService.get().player; - } - } // MediaBrowserServiceCompat methods @Nullable @Override - public BrowserRoot onGetRoot(@NonNull final String clientPackageName, final int clientUid, + public BrowserRoot onGetRoot(@NonNull final String clientPackageName, + final int clientUid, @Nullable final Bundle rootHints) { return mediaBrowserConnector.onGetRoot(clientPackageName, clientUid, rootHints); } @@ -228,8 +224,26 @@ public BrowserRoot onGetRoot(@NonNull final String clientPackageName, final int public void onLoadChildren(@NonNull final String parentId, @NonNull final Result> result) { result.detach(); - final var disposable = mediaBrowserConnector.onLoadChildren(parentId) + final Disposable disposable = mediaBrowserConnector.onLoadChildren(parentId) .subscribe(result::sendResult); compositeDisposableLoadChildren.add(disposable); } + + public static final class LocalBinder extends Binder { + private final WeakReference playerService; + + LocalBinder(final PlayerService playerService) { + this.playerService = new WeakReference<>(playerService); + } + + @Nullable + public PlayerService getService() { + return playerService.get(); + } + + @Nullable + public Player getPlayer() { + return playerService.get().player; + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java index df5b6d1db38..476f7be1b24 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java @@ -23,12 +23,14 @@ import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; import org.schabi.newpipe.database.AppDatabase; +import org.schabi.newpipe.database.history.dao.StreamHistoryDAO; import org.schabi.newpipe.database.history.model.StreamHistoryEntry; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; +import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.local.playlist.LocalPlaylistManager; import org.schabi.newpipe.player.PlayerService; import org.schabi.newpipe.player.playqueue.PlayQueue; @@ -44,11 +46,15 @@ import io.reactivex.rxjava3.disposables.Disposable; public class MediaBrowserConnector implements MediaSessionConnector.PlaybackPreparer { + private static final String TAG = MediaBrowserConnector.class.getSimpleName(); + @NonNull private final PlayerService playerService; - private final @NonNull MediaSessionConnector sessionConnector; - private final @NonNull MediaSessionCompat mediaSession; + @NonNull + private final MediaSessionConnector sessionConnector; + @NonNull + private final MediaSessionCompat mediaSession; private AppDatabase database; private LocalPlaylistManager localPlaylistManager; @@ -63,7 +69,8 @@ public MediaBrowserConnector(@NonNull final PlayerService playerService) { playerService.setSessionToken(mediaSession.getSessionToken()); } - public @NonNull MediaSessionConnector getSessionConnector() { + @NonNull + public MediaSessionConnector getSessionConnector() { return sessionConnector; } @@ -86,32 +93,35 @@ private MediaItem createRootMediaItem(final String mediaId, final String folderN builder.setMediaId(mediaId); builder.setTitle(folderName); - final var extras = new Bundle(); + final Bundle extras = new Bundle(); extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, playerService.getString(R.string.app_name)); builder.setExtras(extras); return new MediaItem(builder.build(), MediaItem.FLAG_BROWSABLE); } - private MediaItem createPlaylistMediaItem(final PlaylistMetadataEntry playlist) { + @NonNull + private MediaItem createPlaylistMediaItem(@NonNull final PlaylistMetadataEntry playlist) { final var builder = new MediaDescriptionCompat.Builder(); builder.setMediaId(createMediaIdForPlaylist(playlist.getUid())) - .setTitle(playlist.name) - .setIconUri(Uri.parse(playlist.thumbnailUrl)); + .setTitle(playlist.name) + .setIconUri(Uri.parse(playlist.thumbnailUrl)); - final var extras = new Bundle(); + final Bundle extras = new Bundle(); extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, playerService.getResources().getString(R.string.tab_bookmarks)); builder.setExtras(extras); return new MediaItem(builder.build(), MediaItem.FLAG_BROWSABLE); } + @NonNull private String createMediaIdForPlaylist(final long playlistId) { return ID_BOOKMARKS + '/' + playlistId; } + @NonNull private MediaItem createPlaylistStreamMediaItem(final long playlistId, - final PlaylistStreamEntry item, + @NonNull final PlaylistStreamEntry item, final int index) { final var builder = new MediaDescriptionCompat.Builder(); builder.setMediaId(createMediaIdForPlaylistIndex(playlistId, index)) @@ -121,6 +131,7 @@ private MediaItem createPlaylistStreamMediaItem(final long playlistId, return new MediaItem(builder.build(), MediaItem.FLAG_PLAYABLE); } + @NonNull private String createMediaIdForPlaylistIndex(final long playlistId, final int index) { return createMediaIdForPlaylist(playlistId) + '/' + index; } @@ -143,7 +154,6 @@ public Single> onLoadChildren(@NonNull final String parentId) { } final List mediaItems = new ArrayList<>(); - final var parentIdUri = Uri.parse(parentId); if (parentId.equals(ID_ROOT)) { mediaItems.add( @@ -153,14 +163,15 @@ public Single> onLoadChildren(@NonNull final String parentId) { createRootMediaItem(ID_HISTORY, playerService.getResources().getString(R.string.action_history))); } else if (parentId.startsWith(ID_BOOKMARKS)) { - final var path = parentIdUri.getPathSegments(); + final Uri parentIdUri = Uri.parse(parentId); + final List path = parentIdUri.getPathSegments(); if (path.size() == 2) { return populateBookmarks(); } else if (path.size() == 3) { - final var playlistId = Long.parseLong(path.get(2)); + final long playlistId = Long.parseLong(path.get(2)); return populatePlaylist(playlistId); } else { - Log.w(TAG, "Unknown playlist uri " + parentId); + Log.w(TAG, "Unknown playlist URI: " + parentId); } } else if (parentId.equals(ID_HISTORY)) { return populateHistory(); @@ -169,17 +180,19 @@ public Single> onLoadChildren(@NonNull final String parentId) { } private Single> populateHistory() { - final var streamHistory = getDatabase().streamHistoryDAO(); + final StreamHistoryDAO streamHistory = getDatabase().streamHistoryDAO(); final var history = streamHistory.getHistory().firstOrError(); - return history.map(items -> - items.stream().map(this::createHistoryMediaItem).collect(Collectors.toList())); + return history.map(items -> items.stream() + .map(this::createHistoryMediaItem) + .collect(Collectors.toList())); } + @NonNull private MediaItem createHistoryMediaItem(@NonNull final StreamHistoryEntry streamHistoryEntry) { final var builder = new MediaDescriptionCompat.Builder(); builder.setMediaId(ID_STREAM + '/' + streamHistoryEntry.getStreamId()) - .setTitle(streamHistoryEntry.getStreamEntity().getTitle()) - .setIconUri(Uri.parse(streamHistoryEntry.getStreamEntity().getThumbnailUrl())); + .setTitle(streamHistoryEntry.getStreamEntity().getTitle()) + .setIconUri(Uri.parse(streamHistoryEntry.getStreamEntity().getThumbnailUrl())); return new MediaItem(builder.build(), MediaItem.FLAG_PLAYABLE); } @@ -198,10 +211,14 @@ private LocalPlaylistManager getPlaylistManager() { return localPlaylistManager; } + // Suppress Sonar warning replace list collection by Stream.toList call, as this method is only + // available in Android API 34 and not currently available with desugaring + @SuppressWarnings("squid:S6204") private Single> populateBookmarks() { final var playlists = getPlaylistManager().getPlaylists().firstOrError(); - return playlists.map(playlist -> - playlist.stream().map(this::createPlaylistMediaItem).collect(Collectors.toList())); + return playlists.map(playlist -> playlist.stream() + .map(this::createPlaylistMediaItem) + .collect(Collectors.toList())); } private Single> populatePlaylist(final long playlistId) { @@ -209,7 +226,7 @@ private Single> populatePlaylist(final long playlistId) { return playlist.map(items -> { final List results = new ArrayList<>(); int index = 0; - for (final var item : items) { + for (final PlaylistStreamEntry item : items) { results.add(createPlaylistStreamMediaItem(playlistId, item, index)); ++index; } @@ -231,36 +248,33 @@ private Single extractPlayQueueFromMediaId(final String mediaId) { if (mediaIdUri == null) { return Single.error(new ContentNotAvailableException("Media ID cannot be parsed")); } - if (mediaId.startsWith(ID_BOOKMARKS)) { - final var path = mediaIdUri.getPathSegments(); - if (path.size() == 4) { - final long playlistId = Long.parseLong(path.get(2)); - final int index = Integer.parseInt(path.get(3)); - - return getPlaylistManager() - .getPlaylistStreams(playlistId) - .firstOrError() - .map(items -> { - final var infoItems = items.stream() - .map(PlaylistStreamEntry::toStreamInfoItem) - .collect(Collectors.toList()); - return new SinglePlayQueue(infoItems, index); - }); - } - } else if (mediaId.startsWith(ID_STREAM)) { - final var path = mediaIdUri.getPathSegments(); - if (path.size() == 3) { - final long streamId = Long.parseLong(path.get(2)); - return getDatabase().streamHistoryDAO().getHistory() - .firstOrError() - .map(items -> { - final var infoItems = items.stream() - .filter(it -> it.getStreamId() == streamId) - .map(StreamHistoryEntry::toStreamInfoItem) - .collect(Collectors.toList()); - return new SinglePlayQueue(infoItems, 0); - }); - } + + final List path = mediaIdUri.getPathSegments(); + + if (mediaId.startsWith(ID_BOOKMARKS) && path.size() == 4) { + final long playlistId = Long.parseLong(path.get(2)); + final int index = Integer.parseInt(path.get(3)); + + return getPlaylistManager() + .getPlaylistStreams(playlistId) + .firstOrError() + .map(items -> { + final List infoItems = items.stream() + .map(PlaylistStreamEntry::toStreamInfoItem) + .collect(Collectors.toList()); + return new SinglePlayQueue(infoItems, index); + }); + } else if (mediaId.startsWith(ID_STREAM) && path.size() == 3) { + final long streamId = Long.parseLong(path.get(2)); + return getDatabase().streamHistoryDAO().getHistory() + .firstOrError() + .map(items -> { + final List infoItems = items.stream() + .filter(it -> it.getStreamId() == streamId) + .map(StreamHistoryEntry::toStreamInfoItem) + .collect(Collectors.toList()); + return new SinglePlayQueue(infoItems, 0); + }); } return Single.error(new ContentNotAvailableException("Media ID cannot be parsed")); @@ -285,7 +299,8 @@ public void onPrepare(final boolean playWhenReady) { } @Override - public void onPrepareFromMediaId(@NonNull final String mediaId, final boolean playWhenReady, + public void onPrepareFromMediaId(@NonNull final String mediaId, + final boolean playWhenReady, @Nullable final Bundle extras) { if (DEBUG) { Log.d(TAG, String.format("MediaBrowserConnector.onPrepareFromMediaId(%s, %s, %s)", @@ -307,22 +322,26 @@ public void onPrepareFromMediaId(@NonNull final String mediaId, final boolean pl } @Override - public void onPrepareFromSearch(@NonNull final String query, final boolean playWhenReady, + public void onPrepareFromSearch(@NonNull final String query, + final boolean playWhenReady, @Nullable final Bundle extras) { disposePrepareOrPlayCommands(); playbackError(R.string.content_not_supported, PlaybackStateCompat.ERROR_CODE_NOT_SUPPORTED); } @Override - public void onPrepareFromUri(@NonNull final Uri uri, final boolean playWhenReady, + public void onPrepareFromUri(@NonNull final Uri uri, + final boolean playWhenReady, @Nullable final Bundle extras) { disposePrepareOrPlayCommands(); playbackError(R.string.content_not_supported, PlaybackStateCompat.ERROR_CODE_NOT_SUPPORTED); } @Override - public boolean onCommand(@NonNull final Player player, @NonNull final String command, - @Nullable final Bundle extras, @Nullable final ResultReceiver cb) { + public boolean onCommand(@NonNull final Player player, + @NonNull final String command, + @Nullable final Bundle extras, + @Nullable final ResultReceiver cb) { return false; } } From a5263195c4c0db36a9ffe6ce6442be11f9b30c15 Mon Sep 17 00:00:00 2001 From: AudricV <74829229+AudricV@users.noreply.github.com> Date: Fri, 11 Aug 2023 13:32:06 +0200 Subject: [PATCH 10/31] Add icons to root media items --- .../mediabrowser/MediaBrowserConnector.java | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java index 476f7be1b24..df85d0e407d 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java @@ -2,6 +2,8 @@ import static org.schabi.newpipe.MainActivity.DEBUG; +import android.content.ContentResolver; +import android.content.res.Resources; import android.net.Uri; import android.os.Bundle; import android.os.ResultReceiver; @@ -11,6 +13,7 @@ import android.support.v4.media.session.PlaybackStateCompat; import android.util.Log; +import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; @@ -88,10 +91,20 @@ public void release() { @NonNull private static final String ID_STREAM = ID_ROOT + "/stream"; - private MediaItem createRootMediaItem(final String mediaId, final String folderName) { + @NonNull + private MediaItem createRootMediaItem(@Nullable final String mediaId, + final String folderName, + @DrawableRes final int iconResId) { final var builder = new MediaDescriptionCompat.Builder(); builder.setMediaId(mediaId); builder.setTitle(folderName); + final Resources resources = playerService.getResources(); + builder.setIconUri(new Uri.Builder() + .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) + .authority(resources.getResourcePackageName(iconResId)) + .appendPath(resources.getResourceTypeName(iconResId)) + .appendPath(resources.getResourceEntryName(iconResId)) + .build()); final Bundle extras = new Bundle(); extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, @@ -158,10 +171,12 @@ public Single> onLoadChildren(@NonNull final String parentId) { if (parentId.equals(ID_ROOT)) { mediaItems.add( createRootMediaItem(ID_BOOKMARKS, - playerService.getResources().getString(R.string.tab_bookmarks))); + playerService.getResources().getString(R.string.tab_bookmarks), + R.drawable.ic_bookmark)); mediaItems.add( createRootMediaItem(ID_HISTORY, - playerService.getResources().getString(R.string.action_history))); + playerService.getResources().getString(R.string.action_history), + R.drawable.ic_history)); } else if (parentId.startsWith(ID_BOOKMARKS)) { final Uri parentIdUri = Uri.parse(parentId); final List path = parentIdUri.getPathSegments(); From 511b014dc07ce411e8b920a9343d103c48a9e582 Mon Sep 17 00:00:00 2001 From: AudricV <74829229+AudricV@users.noreply.github.com> Date: Sun, 20 Aug 2023 17:44:44 +0200 Subject: [PATCH 11/31] Add uploader name of streams as subtitle of MediaItems They should be displayed only by Android Auto if they are known, i.e. when they are not empty. --- .../newpipe/player/mediabrowser/MediaBrowserConnector.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java index df85d0e407d..7a383e09616 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java @@ -139,6 +139,7 @@ private MediaItem createPlaylistStreamMediaItem(final long playlistId, final var builder = new MediaDescriptionCompat.Builder(); builder.setMediaId(createMediaIdForPlaylistIndex(playlistId, index)) .setTitle(item.getStreamEntity().getTitle()) + .setSubtitle(item.getStreamEntity().getUploader()) .setIconUri(Uri.parse(item.getStreamEntity().getThumbnailUrl())); return new MediaItem(builder.build(), MediaItem.FLAG_PLAYABLE); @@ -207,6 +208,7 @@ private MediaItem createHistoryMediaItem(@NonNull final StreamHistoryEntry strea final var builder = new MediaDescriptionCompat.Builder(); builder.setMediaId(ID_STREAM + '/' + streamHistoryEntry.getStreamId()) .setTitle(streamHistoryEntry.getStreamEntity().getTitle()) + .setSubtitle(streamHistoryEntry.getStreamEntity().getUploader()) .setIconUri(Uri.parse(streamHistoryEntry.getStreamEntity().getThumbnailUrl())); return new MediaItem(builder.build(), MediaItem.FLAG_PLAYABLE); From 148b050d5f225a0f9a9a6d2d542df1295ee690f0 Mon Sep 17 00:00:00 2001 From: Haggai Eran Date: Tue, 22 Aug 2023 18:32:59 +0300 Subject: [PATCH 12/31] Update media browsers when the list of local playlist changes --- .../mediabrowser/MediaBrowserConnector.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java index 7a383e09616..3593155efbd 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java @@ -70,6 +70,8 @@ public MediaBrowserConnector(@NonNull final PlayerService playerService) { sessionConnector.setMetadataDeduplicationEnabled(true); sessionConnector.setPlaybackPreparer(this); playerService.setSessionToken(mediaSession.getSessionToken()); + + setupBookmarksNotifications(); } @NonNull @@ -79,6 +81,7 @@ public MediaSessionConnector getSessionConnector() { public void release() { disposePrepareOrPlayCommands(); + disposeBookmarksNotifications(); mediaSession.release(); } @@ -228,6 +231,20 @@ private LocalPlaylistManager getPlaylistManager() { return localPlaylistManager; } + @Nullable Disposable bookmarksNotificationsDisposable; + + private void setupBookmarksNotifications() { + bookmarksNotificationsDisposable = getPlaylistManager().getPlaylists().subscribe( + playlistMetadataEntries -> playerService.notifyChildrenChanged(ID_BOOKMARKS)); + } + + private void disposeBookmarksNotifications() { + if (bookmarksNotificationsDisposable != null) { + bookmarksNotificationsDisposable.dispose(); + bookmarksNotificationsDisposable = null; + } + } + // Suppress Sonar warning replace list collection by Stream.toList call, as this method is only // available in Android API 34 and not currently available with desugaring @SuppressWarnings("squid:S6204") From 2a01f371a1d3b5ce33a18e5c584652f32f807e78 Mon Sep 17 00:00:00 2001 From: Haggai Eran Date: Wed, 24 Jul 2024 20:38:09 +0300 Subject: [PATCH 13/31] android auto: fix navigation tab colors and cut text --- .../player/mediabrowser/MediaBrowserConnector.java | 6 +++--- app/src/main/res/drawable/ic_bookmark_white.xml | 10 ++++++++++ app/src/main/res/drawable/ic_history_white.xml | 10 ++++++++++ app/src/main/res/values/strings.xml | 1 + 4 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 app/src/main/res/drawable/ic_bookmark_white.xml create mode 100644 app/src/main/res/drawable/ic_history_white.xml diff --git a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java index 3593155efbd..7d67e3865e2 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java @@ -175,12 +175,12 @@ public Single> onLoadChildren(@NonNull final String parentId) { if (parentId.equals(ID_ROOT)) { mediaItems.add( createRootMediaItem(ID_BOOKMARKS, - playerService.getResources().getString(R.string.tab_bookmarks), - R.drawable.ic_bookmark)); + playerService.getResources().getString(R.string.tab_bookmarks_short), + R.drawable.ic_bookmark_white)); mediaItems.add( createRootMediaItem(ID_HISTORY, playerService.getResources().getString(R.string.action_history), - R.drawable.ic_history)); + R.drawable.ic_history_white)); } else if (parentId.startsWith(ID_BOOKMARKS)) { final Uri parentIdUri = Uri.parse(parentId); final List path = parentIdUri.getPathSegments(); diff --git a/app/src/main/res/drawable/ic_bookmark_white.xml b/app/src/main/res/drawable/ic_bookmark_white.xml new file mode 100644 index 00000000000..a04ed256e9d --- /dev/null +++ b/app/src/main/res/drawable/ic_bookmark_white.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_history_white.xml b/app/src/main/res/drawable/ic_history_white.xml new file mode 100644 index 00000000000..585285b890c --- /dev/null +++ b/app/src/main/res/drawable/ic_history_white.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2c27e6cbb76..529ef0d9d6b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -33,6 +33,7 @@ Show info Subscriptions Bookmarked Playlists + Playlists Choose Tab Background Popup From 6871c96971c952db16b18144b550c2261577a2c1 Mon Sep 17 00:00:00 2001 From: Haggai Eran Date: Fri, 2 Aug 2024 13:42:49 +0300 Subject: [PATCH 14/31] PlaylistMetadataEntry: add interface method to get the thumbnail Url --- .../schabi/newpipe/database/playlist/PlaylistLocalItem.java | 2 ++ .../newpipe/database/playlist/PlaylistMetadataEntry.java | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java index 072c49e2c07..a974a09d0da 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java @@ -10,4 +10,6 @@ public interface PlaylistLocalItem extends LocalItem { long getUid(); void setDisplayIndex(long displayIndex); + + String getThumbnailUrl(); } diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java index 03a1e1e308a..4b0338b390e 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java @@ -71,4 +71,9 @@ public long getUid() { public void setDisplayIndex(final long displayIndex) { this.displayIndex = displayIndex; } + + @Override + public String getThumbnailUrl() { + return thumbnailUrl; + } } From b795f4d27c5e072b8ec41d64ef1c7595f8837e02 Mon Sep 17 00:00:00 2001 From: Haggai Eran Date: Fri, 2 Aug 2024 13:43:25 +0300 Subject: [PATCH 15/31] RemotePlaylistManager: add helper method to get a playlist by its uid --- .../schabi/newpipe/local/playlist/RemotePlaylistManager.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/RemotePlaylistManager.java b/app/src/main/java/org/schabi/newpipe/local/playlist/RemotePlaylistManager.java index 4cc51f7525e..7104f59629a 100644 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/RemotePlaylistManager.java +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/RemotePlaylistManager.java @@ -26,6 +26,10 @@ public Flowable> getPlaylists() { return playlistRemoteTable.getPlaylists().subscribeOn(Schedulers.io()); } + public Flowable> getPlaylist(final long playlistId) { + return playlistRemoteTable.getPlaylist(playlistId).subscribeOn(Schedulers.io()); + } + public Flowable> getPlaylist(final PlaylistInfo info) { return playlistRemoteTable.getPlaylist(info.getServiceId(), info.getUrl()) .subscribeOn(Schedulers.io()); From e9536c1865cf2ff9762c287f78448f1c9a31f8fa Mon Sep 17 00:00:00 2001 From: Haggai Eran Date: Fri, 2 Aug 2024 13:44:27 +0300 Subject: [PATCH 16/31] media browser: expose remote playlists together with local playlists This is similar to how they are shown in the app UI. --- .../mediabrowser/MediaBrowserConnector.java | 163 ++++++++++++++---- 1 file changed, 126 insertions(+), 37 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java index 7d67e3865e2..99099e30837 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java @@ -12,6 +12,7 @@ import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.PlaybackStateCompat; import android.util.Log; +import android.util.Pair; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; @@ -28,23 +29,30 @@ import org.schabi.newpipe.database.AppDatabase; import org.schabi.newpipe.database.history.dao.StreamHistoryDAO; import org.schabi.newpipe.database.history.model.StreamHistoryEntry; -import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; +import org.schabi.newpipe.database.playlist.PlaylistLocalItem; import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; +import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; +import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException; import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import org.schabi.newpipe.local.bookmark.MergedPlaylistManager; import org.schabi.newpipe.local.playlist.LocalPlaylistManager; +import org.schabi.newpipe.local.playlist.RemotePlaylistManager; import org.schabi.newpipe.player.PlayerService; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue; +import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.NavigationHelper; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; +import java.util.stream.IntStream; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.core.Flowable; import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.disposables.Disposable; @@ -61,6 +69,7 @@ public class MediaBrowserConnector implements MediaSessionConnector.PlaybackPrep private AppDatabase database; private LocalPlaylistManager localPlaylistManager; + private RemotePlaylistManager remotePlaylistManager; private Disposable prepareOrPlayDisposable; public MediaBrowserConnector(@NonNull final PlayerService playerService) { @@ -94,6 +103,11 @@ public void release() { @NonNull private static final String ID_STREAM = ID_ROOT + "/stream"; + @NonNull + private static final String ID_LOCAL = "local"; + @NonNull + private static final String ID_REMOTE = "remote"; + @NonNull private MediaItem createRootMediaItem(@Nullable final String mediaId, final String folderName, @@ -117,11 +131,12 @@ private MediaItem createRootMediaItem(@Nullable final String mediaId, } @NonNull - private MediaItem createPlaylistMediaItem(@NonNull final PlaylistMetadataEntry playlist) { + private MediaItem createPlaylistMediaItem(@NonNull final PlaylistLocalItem playlist) { final var builder = new MediaDescriptionCompat.Builder(); - builder.setMediaId(createMediaIdForPlaylist(playlist.getUid())) - .setTitle(playlist.name) - .setIconUri(Uri.parse(playlist.thumbnailUrl)); + final boolean remote = playlist instanceof PlaylistRemoteEntity; + builder.setMediaId(createMediaIdForPlaylist(remote, playlist.getUid())) + .setTitle(playlist.getOrderingName()) + .setIconUri(Uri.parse(playlist.getThumbnailUrl())); final Bundle extras = new Bundle(); extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, @@ -131,16 +146,16 @@ private MediaItem createPlaylistMediaItem(@NonNull final PlaylistMetadataEntry p } @NonNull - private String createMediaIdForPlaylist(final long playlistId) { - return ID_BOOKMARKS + '/' + playlistId; + private String createMediaIdForPlaylist(final boolean remote, final long playlistId) { + return ID_BOOKMARKS + '/' + (remote ? ID_REMOTE : ID_LOCAL) + '/' + playlistId; } @NonNull - private MediaItem createPlaylistStreamMediaItem(final long playlistId, - @NonNull final PlaylistStreamEntry item, - final int index) { + private MediaItem createLocalPlaylistStreamMediaItem(final long playlistId, + @NonNull final PlaylistStreamEntry item, + final int index) { final var builder = new MediaDescriptionCompat.Builder(); - builder.setMediaId(createMediaIdForPlaylistIndex(playlistId, index)) + builder.setMediaId(createMediaIdForPlaylistIndex(false, playlistId, index)) .setTitle(item.getStreamEntity().getTitle()) .setSubtitle(item.getStreamEntity().getUploader()) .setIconUri(Uri.parse(item.getStreamEntity().getThumbnailUrl())); @@ -149,8 +164,25 @@ private MediaItem createPlaylistStreamMediaItem(final long playlistId, } @NonNull - private String createMediaIdForPlaylistIndex(final long playlistId, final int index) { - return createMediaIdForPlaylist(playlistId) + '/' + index; + private MediaItem createRemotePlaylistStreamMediaItem(final long playlistId, + @NonNull final StreamInfoItem item, + final int index) { + final var builder = new MediaDescriptionCompat.Builder(); + builder.setMediaId(createMediaIdForPlaylistIndex(true, playlistId, index)) + .setTitle(item.getName()) + .setSubtitle(item.getUploaderName()); + final var thumbnails = item.getThumbnails(); + if (!thumbnails.isEmpty()) { + builder.setIconUri(Uri.parse(thumbnails.get(0).getUrl())); + } + + return new MediaItem(builder.build(), MediaItem.FLAG_PLAYABLE); + } + + @NonNull + private String createMediaIdForPlaylistIndex(final boolean remote, final long playlistId, + final int index) { + return createMediaIdForPlaylist(remote, playlistId) + '/' + index; } @Nullable @@ -186,12 +218,16 @@ public Single> onLoadChildren(@NonNull final String parentId) { final List path = parentIdUri.getPathSegments(); if (path.size() == 2) { return populateBookmarks(); - } else if (path.size() == 3) { - final long playlistId = Long.parseLong(path.get(2)); - return populatePlaylist(playlistId); - } else { - Log.w(TAG, "Unknown playlist URI: " + parentId); + } else if (path.size() == 4) { + final String localOrRemote = path.get(2); + final long playlistId = Long.parseLong(path.get(3)); + if (localOrRemote.equals(ID_LOCAL)) { + return populateLocalPlaylist(playlistId); + } else if (localOrRemote.equals(ID_REMOTE)) { + return populateRemotePlaylist(playlistId); + } } + Log.w(TAG, "Unknown playlist URI: " + parentId); } else if (parentId.equals(ID_HISTORY)) { return populateHistory(); } @@ -224,17 +260,21 @@ private AppDatabase getDatabase() { return database; } - private LocalPlaylistManager getPlaylistManager() { + private Flowable> getPlaylists() { if (localPlaylistManager == null) { localPlaylistManager = new LocalPlaylistManager(getDatabase()); } - return localPlaylistManager; + if (remotePlaylistManager == null) { + remotePlaylistManager = new RemotePlaylistManager(getDatabase()); + } + return MergedPlaylistManager.getMergedOrderedPlaylists(localPlaylistManager, + remotePlaylistManager); } @Nullable Disposable bookmarksNotificationsDisposable; private void setupBookmarksNotifications() { - bookmarksNotificationsDisposable = getPlaylistManager().getPlaylists().subscribe( + bookmarksNotificationsDisposable = getPlaylists().subscribe( playlistMetadataEntries -> playerService.notifyChildrenChanged(ID_BOOKMARKS)); } @@ -249,25 +289,59 @@ private void disposeBookmarksNotifications() { // available in Android API 34 and not currently available with desugaring @SuppressWarnings("squid:S6204") private Single> populateBookmarks() { - final var playlists = getPlaylistManager().getPlaylists().firstOrError(); + final var playlists = getPlaylists().firstOrError(); return playlists.map(playlist -> playlist.stream() .map(this::createPlaylistMediaItem) .collect(Collectors.toList())); } - private Single> populatePlaylist(final long playlistId) { - final var playlist = getPlaylistManager().getPlaylistStreams(playlistId).firstOrError(); + private Single> populateLocalPlaylist(final long playlistId) { + final var playlist = localPlaylistManager.getPlaylistStreams(playlistId).firstOrError(); return playlist.map(items -> { final List results = new ArrayList<>(); int index = 0; for (final PlaylistStreamEntry item : items) { - results.add(createPlaylistStreamMediaItem(playlistId, item, index)); + results.add(createLocalPlaylistStreamMediaItem(playlistId, item, index)); ++index; } return results; }); } + private Single>> getRemotePlaylist(final long playlistId) { + final var playlistFlow = remotePlaylistManager.getPlaylist(playlistId).firstOrError(); + return playlistFlow.flatMap(item -> { + final var playlist = item.get(0); + final var playlistInfo = ExtractorHelper.getPlaylistInfo(playlist.getServiceId(), + playlist.getUrl(), false); + return playlistInfo.flatMap(info -> { + final var infoItemsPage = info.getRelatedItems(); + + if (!info.getErrors().isEmpty()) { + final List errors = new ArrayList<>(info.getErrors()); + + errors.removeIf(ContentNotSupportedException.class::isInstance); + + if (!errors.isEmpty()) { + return Single.error(errors.get(0)); + } + } + + return Single.just(IntStream.range(0, infoItemsPage.size()) + .mapToObj(i -> Pair.create(infoItemsPage.get(i), i)) + .toList()); + }); + }); + } + + private Single> populateRemotePlaylist(final long playlistId) { + return getRemotePlaylist(playlistId).map(items -> + items.stream().map(pair -> + createRemotePlaylistStreamMediaItem(playlistId, pair.first, pair.second) + ).toList() + ); + } + private void playbackError(@StringRes final int resId, final int code) { playerService.stopForImmediateReusing(); sessionConnector.setCustomErrorMessage(playerService.getString(resId), code); @@ -277,6 +351,24 @@ private void playbackError(@NonNull final ErrorInfo errorInfo) { playbackError(errorInfo.getMessageStringId(), PlaybackStateCompat.ERROR_CODE_APP_ERROR); } + private Single extractLocalPlayQueue(final long playlistId, final int index) { + return localPlaylistManager.getPlaylistStreams(playlistId) + .firstOrError() + .map(items -> { + final List infoItems = items.stream() + .map(PlaylistStreamEntry::toStreamInfoItem) + .collect(Collectors.toList()); + return new SinglePlayQueue(infoItems, index); + }); + } + + private Single extractRemotePlayQueue(final long playlistId, final int index) { + return getRemotePlaylist(playlistId).map(items -> { + final var infoItems = items.stream().map(pair -> pair.first).toList(); + return new SinglePlayQueue(infoItems, index); + }); + } + private Single extractPlayQueueFromMediaId(final String mediaId) { final Uri mediaIdUri = Uri.parse(mediaId); if (mediaIdUri == null) { @@ -285,19 +377,16 @@ private Single extractPlayQueueFromMediaId(final String mediaId) { final List path = mediaIdUri.getPathSegments(); - if (mediaId.startsWith(ID_BOOKMARKS) && path.size() == 4) { - final long playlistId = Long.parseLong(path.get(2)); - final int index = Integer.parseInt(path.get(3)); + if (mediaId.startsWith(ID_BOOKMARKS) && path.size() == 5) { + final String localOrRemote = path.get(2); + final long playlistId = Long.parseLong(path.get(3)); + final int index = Integer.parseInt(path.get(4)); - return getPlaylistManager() - .getPlaylistStreams(playlistId) - .firstOrError() - .map(items -> { - final List infoItems = items.stream() - .map(PlaylistStreamEntry::toStreamInfoItem) - .collect(Collectors.toList()); - return new SinglePlayQueue(infoItems, index); - }); + if (localOrRemote.equals(ID_LOCAL)) { + return extractLocalPlayQueue(playlistId, index); + } else { + return extractRemotePlayQueue(playlistId, index); + } } else if (mediaId.startsWith(ID_STREAM) && path.size() == 3) { final long streamId = Long.parseLong(path.get(2)); return getDatabase().streamHistoryDAO().getHistory() From 6cc811fdf1d4f7a882ab2d95ceb67d3bacfd658f Mon Sep 17 00:00:00 2001 From: Haggai Eran Date: Sun, 4 Aug 2024 09:01:46 +0300 Subject: [PATCH 17/31] media browser: support searching Also improve parser code to simplify passing URLs within a media ID. --- .../schabi/newpipe/player/PlayerService.java | 7 + .../mediabrowser/MediaBrowserConnector.java | 392 +++++++++++++++--- 2 files changed, 337 insertions(+), 62 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayerService.java b/app/src/main/java/org/schabi/newpipe/player/PlayerService.java index 55fdfd3a8ac..e088290c921 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PlayerService.java +++ b/app/src/main/java/org/schabi/newpipe/player/PlayerService.java @@ -229,6 +229,13 @@ public void onLoadChildren(@NonNull final String parentId, compositeDisposableLoadChildren.add(disposable); } + @Override + public void onSearch(@NonNull final String query, + final Bundle extras, + @NonNull final Result> result) { + mediaBrowserConnector.onSearch(query, result); + } + public static final class LocalBinder extends Binder { private final WeakReference playerService; diff --git a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java index 99099e30837..18133311e8c 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java @@ -24,6 +24,7 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector; +import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; import org.schabi.newpipe.database.AppDatabase; @@ -34,20 +35,32 @@ import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.UserAction; +import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.extractor.ListInfo; +import org.schabi.newpipe.extractor.channel.ChannelInfoItem; import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException; +import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler; +import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.local.bookmark.MergedPlaylistManager; +import org.schabi.newpipe.extractor.search.SearchExtractor; +import org.schabi.newpipe.extractor.search.SearchInfo; import org.schabi.newpipe.local.playlist.LocalPlaylistManager; import org.schabi.newpipe.local.playlist.RemotePlaylistManager; import org.schabi.newpipe.player.PlayerService; +import org.schabi.newpipe.player.playqueue.ChannelTabPlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue; +import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue; +import org.schabi.newpipe.util.ChannelTabHelper; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.ServiceHelper; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -56,6 +69,9 @@ import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.core.SingleSource; +import io.reactivex.rxjava3.schedulers.Schedulers; + public class MediaBrowserConnector implements MediaSessionConnector.PlaybackPreparer { private static final String TAG = MediaBrowserConnector.class.getSimpleName(); @@ -71,6 +87,7 @@ public class MediaBrowserConnector implements MediaSessionConnector.PlaybackPrep private LocalPlaylistManager localPlaylistManager; private RemotePlaylistManager remotePlaylistManager; private Disposable prepareOrPlayDisposable; + private Disposable searchDisposable; public MediaBrowserConnector(@NonNull final PlayerService playerService) { this.playerService = playerService; @@ -95,18 +112,28 @@ public void release() { } @NonNull - private static final String ID_ROOT = "//${BuildConfig.APPLICATION_ID}/r"; + private static final String ID_AUTHORITY = BuildConfig.APPLICATION_ID; + @NonNull + private static final String ID_ROOT = "//" + ID_AUTHORITY; @NonNull - private static final String ID_BOOKMARKS = ID_ROOT + "/playlists"; + private static final String ID_BOOKMARKS = "playlists"; @NonNull - private static final String ID_HISTORY = ID_ROOT + "/history"; + private static final String ID_HISTORY = "history"; @NonNull - private static final String ID_STREAM = ID_ROOT + "/stream"; + private static final String ID_INFO_ITEM = "item"; @NonNull private static final String ID_LOCAL = "local"; @NonNull private static final String ID_REMOTE = "remote"; + @NonNull + private static final String ID_URL = "url"; + @NonNull + private static final String ID_STREAM = "stream"; + @NonNull + private static final String ID_PLAYLIST = "playlist"; + @NonNull + private static final String ID_CHANNEL = "channel"; @NonNull private MediaItem createRootMediaItem(@Nullable final String mediaId, @@ -134,7 +161,7 @@ private MediaItem createRootMediaItem(@Nullable final String mediaId, private MediaItem createPlaylistMediaItem(@NonNull final PlaylistLocalItem playlist) { final var builder = new MediaDescriptionCompat.Builder(); final boolean remote = playlist instanceof PlaylistRemoteEntity; - builder.setMediaId(createMediaIdForPlaylist(remote, playlist.getUid())) + builder.setMediaId(createMediaIdForInfoItem(remote, playlist.getUid())) .setTitle(playlist.getOrderingName()) .setIconUri(Uri.parse(playlist.getThumbnailUrl())); @@ -145,9 +172,82 @@ private MediaItem createPlaylistMediaItem(@NonNull final PlaylistLocalItem playl return new MediaItem(builder.build(), MediaItem.FLAG_BROWSABLE); } + private MediaItem createInfoItemMediaItem(@NonNull final InfoItem item) { + final var builder = new MediaDescriptionCompat.Builder(); + builder.setMediaId(createMediaIdForInfoItem(item)) + .setTitle(item.getName()); + + switch (item.getInfoType()) { + case STREAM: + builder.setSubtitle(((StreamInfoItem) item).getUploaderName()); + break; + case PLAYLIST: + builder.setSubtitle(((PlaylistInfoItem) item).getUploaderName()); + break; + case CHANNEL: + builder.setSubtitle(((ChannelInfoItem) item).getDescription()); + break; + default: + break; + } + final var thumbnails = item.getThumbnails(); + if (!thumbnails.isEmpty()) { + builder.setIconUri(Uri.parse(thumbnails.get(0).getUrl())); + } + return new MediaItem(builder.build(), MediaItem.FLAG_PLAYABLE); + } + + @NonNull + private Uri.Builder buildMediaId() { + return new Uri.Builder().authority(ID_AUTHORITY); + } + + @NonNull + private Uri.Builder buildPlaylistMediaId(final String playlistType) { + return buildMediaId() + .appendPath(ID_BOOKMARKS) + .appendPath(playlistType); + } + @NonNull - private String createMediaIdForPlaylist(final boolean remote, final long playlistId) { - return ID_BOOKMARKS + '/' + (remote ? ID_REMOTE : ID_LOCAL) + '/' + playlistId; + private Uri.Builder buildLocalPlaylistItemMediaId(final boolean remote, final long playlistId) { + return buildPlaylistMediaId(remote ? ID_REMOTE : ID_LOCAL) + .appendPath(Long.toString(playlistId)); + } + + private static String infoItemTypeToString(final InfoItem.InfoType type) { + return switch (type) { + case STREAM -> ID_STREAM; + case PLAYLIST -> ID_PLAYLIST; + case CHANNEL -> ID_CHANNEL; + default -> + throw new IllegalStateException("Unexpected value: " + type); + }; + } + + private static InfoItem.InfoType infoItemTypeFromString(final String type) { + return switch (type) { + case ID_STREAM -> InfoItem.InfoType.STREAM; + case ID_PLAYLIST -> InfoItem.InfoType.PLAYLIST; + case ID_CHANNEL -> InfoItem.InfoType.CHANNEL; + default -> + throw new IllegalStateException("Unexpected value: " + type); + }; + } + + @NonNull + private Uri.Builder buildInfoItemMediaId(@NonNull final InfoItem item) { + return buildMediaId() + .appendPath(ID_INFO_ITEM) + .appendPath(infoItemTypeToString(item.getInfoType())) + .appendPath(Integer.toString(item.getServiceId())) + .appendQueryParameter(ID_URL, item.getUrl()); + } + + @NonNull + private String createMediaIdForInfoItem(final boolean remote, final long playlistId) { + return buildLocalPlaylistItemMediaId(remote, playlistId) + .build().toString(); } @NonNull @@ -182,7 +282,14 @@ private MediaItem createRemotePlaylistStreamMediaItem(final long playlistId, @NonNull private String createMediaIdForPlaylistIndex(final boolean remote, final long playlistId, final int index) { - return createMediaIdForPlaylist(remote, playlistId) + '/' + index; + return buildLocalPlaylistItemMediaId(remote, playlistId) + .appendPath(Integer.toString(index)) + .build().toString(); + } + + @NonNull + private String createMediaIdForInfoItem(@NonNull final InfoItem item) { + return buildInfoItemMediaId(item).build().toString(); } @Nullable @@ -194,7 +301,10 @@ public MediaBrowserServiceCompat.BrowserRoot onGetRoot(@NonNull final String cli clientPackageName, clientUid, rootHints)); } - return new MediaBrowserServiceCompat.BrowserRoot(ID_ROOT, null); + final Bundle extras = new Bundle(); + extras.putBoolean( + MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, true); + return new MediaBrowserServiceCompat.BrowserRoot(ID_ROOT, extras); } public Single> onLoadChildren(@NonNull final String parentId) { @@ -202,36 +312,56 @@ public Single> onLoadChildren(@NonNull final String parentId) { Log.d(TAG, String.format("MediaBrowserService.onLoadChildren(%s)", parentId)); } - final List mediaItems = new ArrayList<>(); - - if (parentId.equals(ID_ROOT)) { - mediaItems.add( - createRootMediaItem(ID_BOOKMARKS, - playerService.getResources().getString(R.string.tab_bookmarks_short), - R.drawable.ic_bookmark_white)); - mediaItems.add( - createRootMediaItem(ID_HISTORY, - playerService.getResources().getString(R.string.action_history), - R.drawable.ic_history_white)); - } else if (parentId.startsWith(ID_BOOKMARKS)) { + + try { final Uri parentIdUri = Uri.parse(parentId); - final List path = parentIdUri.getPathSegments(); - if (path.size() == 2) { - return populateBookmarks(); - } else if (path.size() == 4) { - final String localOrRemote = path.get(2); - final long playlistId = Long.parseLong(path.get(3)); - if (localOrRemote.equals(ID_LOCAL)) { - return populateLocalPlaylist(playlistId); - } else if (localOrRemote.equals(ID_REMOTE)) { - return populateRemotePlaylist(playlistId); - } + if (parentIdUri == null) { + throw parseError(); } - Log.w(TAG, "Unknown playlist URI: " + parentId); - } else if (parentId.equals(ID_HISTORY)) { - return populateHistory(); + + final List path = new ArrayList<>(parentIdUri.getPathSegments()); + + if (path.isEmpty()) { + final List mediaItems = new ArrayList<>(); + mediaItems.add( + createRootMediaItem(ID_BOOKMARKS, + playerService.getResources().getString( + R.string.tab_bookmarks_short), + R.drawable.ic_bookmark_white)); + mediaItems.add( + createRootMediaItem(ID_HISTORY, + playerService.getResources().getString(R.string.action_history), + R.drawable.ic_history_white)); + return Single.just(mediaItems); + } + + final String uriType = path.get(0); + path.remove(0); + + switch (uriType) { + case ID_BOOKMARKS: + if (path.isEmpty()) { + return populateBookmarks(); + } + if (path.size() == 2) { + final String localOrRemote = path.get(0); + final long playlistId = Long.parseLong(path.get(1)); + if (localOrRemote.equals(ID_LOCAL)) { + return populateLocalPlaylist(playlistId); + } else if (localOrRemote.equals(ID_REMOTE)) { + return populateRemotePlaylist(playlistId); + } + } + Log.w(TAG, "Unknown playlist URI: " + parentId); + throw parseError(); + case ID_HISTORY: + return populateHistory(); + default: + throw parseError(); + } + } catch (final ContentNotAvailableException e) { + return Single.error(e); } - return Single.just(mediaItems); } private Single> populateHistory() { @@ -245,7 +375,11 @@ private Single> populateHistory() { @NonNull private MediaItem createHistoryMediaItem(@NonNull final StreamHistoryEntry streamHistoryEntry) { final var builder = new MediaDescriptionCompat.Builder(); - builder.setMediaId(ID_STREAM + '/' + streamHistoryEntry.getStreamId()) + final var mediaId = buildMediaId() + .appendPath(ID_HISTORY) + .appendPath(Long.toString(streamHistoryEntry.getStreamId())) + .build().toString(); + builder.setMediaId(mediaId) .setTitle(streamHistoryEntry.getStreamEntity().getTitle()) .setSubtitle(streamHistoryEntry.getStreamEntity().getUploader()) .setIconUri(Uri.parse(streamHistoryEntry.getStreamEntity().getThumbnailUrl())); @@ -369,38 +503,119 @@ private Single extractRemotePlayQueue(final long playlistId, final in }); } + private static ContentNotAvailableException parseError() { + return new ContentNotAvailableException("Failed to parse media ID"); + } + private Single extractPlayQueueFromMediaId(final String mediaId) { - final Uri mediaIdUri = Uri.parse(mediaId); - if (mediaIdUri == null) { - return Single.error(new ContentNotAvailableException("Media ID cannot be parsed")); + try { + final Uri mediaIdUri = Uri.parse(mediaId); + if (mediaIdUri == null) { + throw parseError(); + } + + final List path = new ArrayList<>(mediaIdUri.getPathSegments()); + + if (path.isEmpty()) { + throw parseError(); + } + + final String uriType = path.get(0); + path.remove(0); + + return switch (uriType) { + case ID_BOOKMARKS -> extractPlayQueueFromPlaylistMediaId(path, + mediaIdUri.getQueryParameter(ID_URL)); + case ID_HISTORY -> extractPlayQueueFromHistoryMediaId(path); + case ID_INFO_ITEM -> extractPlayQueueFromInfoItemMediaId(path, + mediaIdUri.getQueryParameter(ID_URL)); + default -> throw parseError(); + }; + } catch (final ContentNotAvailableException e) { + return Single.error(e); } + } - final List path = mediaIdUri.getPathSegments(); + private Single + extractPlayQueueFromPlaylistMediaId( + @NonNull final List path, + @Nullable final String url) throws ContentNotAvailableException { + if (path.isEmpty()) { + throw parseError(); + } - if (mediaId.startsWith(ID_BOOKMARKS) && path.size() == 5) { - final String localOrRemote = path.get(2); - final long playlistId = Long.parseLong(path.get(3)); - final int index = Integer.parseInt(path.get(4)); + final String playlistType = path.get(0); + path.remove(0); - if (localOrRemote.equals(ID_LOCAL)) { - return extractLocalPlayQueue(playlistId, index); - } else { - return extractRemotePlayQueue(playlistId, index); - } - } else if (mediaId.startsWith(ID_STREAM) && path.size() == 3) { - final long streamId = Long.parseLong(path.get(2)); - return getDatabase().streamHistoryDAO().getHistory() - .firstOrError() - .map(items -> { - final List infoItems = items.stream() - .filter(it -> it.getStreamId() == streamId) - .map(StreamHistoryEntry::toStreamInfoItem) - .collect(Collectors.toList()); - return new SinglePlayQueue(infoItems, 0); - }); + switch (playlistType) { + case ID_LOCAL, ID_REMOTE: + if (path.size() != 2) { + throw parseError(); + } + final long playlistId = Long.parseLong(path.get(0)); + final int index = Integer.parseInt(path.get(1)); + return playlistType.equals(ID_LOCAL) + ? extractLocalPlayQueue(playlistId, index) + : extractRemotePlayQueue(playlistId, index); + case ID_URL: + if (path.size() != 1) { + throw parseError(); + } + + final int serviceId = Integer.parseInt(path.get(0)); + return ExtractorHelper.getPlaylistInfo(serviceId, url, false) + .map(PlaylistPlayQueue::new); + default: + throw parseError(); + } + } + + private Single extractPlayQueueFromHistoryMediaId( + final List path) throws ContentNotAvailableException { + if (path.size() != 1) { + throw parseError(); } - return Single.error(new ContentNotAvailableException("Media ID cannot be parsed")); + final long streamId = Long.parseLong(path.get(0)); + return getDatabase().streamHistoryDAO().getHistory() + .firstOrError() + .map(items -> { + final List infoItems = items.stream() + .filter(it -> it.getStreamId() == streamId) + .map(StreamHistoryEntry::toStreamInfoItem) + .collect(Collectors.toList()); + return new SinglePlayQueue(infoItems, 0); + }); + } + + private static Single extractPlayQueueFromInfoItemMediaId( + final List path, final String url) throws ContentNotAvailableException { + if (path.size() != 2) { + throw parseError(); + } + final var infoItemType = infoItemTypeFromString(path.get(0)); + final int serviceId = Integer.parseInt(path.get(1)); + return switch (infoItemType) { + case STREAM -> ExtractorHelper.getStreamInfo(serviceId, url, false) + .map(SinglePlayQueue::new); + case PLAYLIST -> ExtractorHelper.getPlaylistInfo(serviceId, url, false) + .map(PlaylistPlayQueue::new); + case CHANNEL -> ExtractorHelper.getChannelInfo(serviceId, url, false) + .map(info -> { + final Optional playableTab = info.getTabs() + .stream() + .filter(ChannelTabHelper::isStreamsTab) + .findFirst(); + + if (playableTab.isPresent()) { + return new ChannelTabPlayQueue(serviceId, + new ListLinkHandler(playableTab.get())); + } else { + throw new ContentNotAvailableException("No streams tab found"); + } + }); + default -> throw parseError(); + }; } @Override @@ -432,6 +647,7 @@ public void onPrepareFromMediaId(@NonNull final String mediaId, disposePrepareOrPlayCommands(); prepareOrPlayDisposable = extractPlayQueueFromMediaId(mediaId) + .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( playQueue -> { @@ -448,6 +664,45 @@ public void onPrepareFromMediaId(@NonNull final String mediaId, public void onPrepareFromSearch(@NonNull final String query, final boolean playWhenReady, @Nullable final Bundle extras) { + disposePrepareOrPlayCommands(); + playbackError(R.string.content_not_supported, + PlaybackStateCompat.ERROR_CODE_NOT_SUPPORTED); + } + + private @NonNull Single searchMusicBySongTitle(final String query) { + final var serviceId = ServiceHelper.getSelectedServiceId(playerService); + return ExtractorHelper.searchFor(serviceId, query, + new ArrayList<>(), ""); + } + + private @NonNull SingleSource> + mediaItemsFromInfoItemList(final ListInfo result) { + final List exceptions = result.getErrors(); + if (!exceptions.isEmpty() + && !(exceptions.size() == 1 + && exceptions.get(0) instanceof SearchExtractor.NothingFoundException)) { + return Single.error(exceptions.get(0)); + } + + final List items = result.getRelatedItems(); + if (items.isEmpty()) { + return Single.error(new NullPointerException("Got no search results.")); + } + try { + final List results = items.stream() + .filter(item -> + item.getInfoType() == InfoItem.InfoType.STREAM + || item.getInfoType() == InfoItem.InfoType.PLAYLIST + || item.getInfoType() == InfoItem.InfoType.CHANNEL) + .map(this::createInfoItemMediaItem).toList(); + return Single.just(results); + } catch (final Exception e) { + return Single.error(e); + } + } + + private void handleSearchError(final Throwable throwable) { + Log.e(TAG, "Search error: " + throwable); disposePrepareOrPlayCommands(); playbackError(R.string.content_not_supported, PlaybackStateCompat.ERROR_CODE_NOT_SUPPORTED); } @@ -467,4 +722,17 @@ public boolean onCommand(@NonNull final Player player, @Nullable final ResultReceiver cb) { return false; } + + public void onSearch(@NonNull final String query, + @NonNull final MediaBrowserServiceCompat.Result> result) { + result.detach(); + if (searchDisposable != null) { + searchDisposable.dispose(); + } + searchDisposable = searchMusicBySongTitle(query) + .flatMap(this::mediaItemsFromInfoItemList) + .subscribeOn(Schedulers.io()) + .subscribe(result::sendResult, + this::handleSearchError); + } } From 8fc659cb6c21f37ae67dd5d8224472541a7c1888 Mon Sep 17 00:00:00 2001 From: Haggai Eran Date: Fri, 16 Aug 2024 17:44:20 +0300 Subject: [PATCH 18/31] media browser: clean up Uri.parse() null checks --- .../player/mediabrowser/MediaBrowserConnector.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java index 18133311e8c..e7856db067f 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java +++ b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java @@ -315,10 +315,6 @@ public Single> onLoadChildren(@NonNull final String parentId) { try { final Uri parentIdUri = Uri.parse(parentId); - if (parentIdUri == null) { - throw parseError(); - } - final List path = new ArrayList<>(parentIdUri.getPathSegments()); if (path.isEmpty()) { @@ -510,10 +506,6 @@ private static ContentNotAvailableException parseError() { private Single extractPlayQueueFromMediaId(final String mediaId) { try { final Uri mediaIdUri = Uri.parse(mediaId); - if (mediaIdUri == null) { - throw parseError(); - } - final List path = new ArrayList<>(mediaIdUri.getPathSegments()); if (path.isEmpty()) { From 715e82944e78ba3d66dbed77ad7462d33d023292 Mon Sep 17 00:00:00 2001 From: Siddhesh Naik Date: Tue, 3 Sep 2024 05:29:57 +0530 Subject: [PATCH 19/31] LocalItem/PlaylistLocalItem: convert to Kotlin --- .../schabi/newpipe/database/LocalItem.java | 13 --------- .../org/schabi/newpipe/database/LocalItem.kt | 18 ++++++++++++ .../history/model/StreamHistoryEntry.kt | 20 ++++++------- .../database/playlist/PlaylistLocalItem.java | 15 ---------- .../database/playlist/PlaylistLocalItem.kt | 28 ++++++++++++++++++ .../database/playlist/PlaylistStreamEntry.kt | 29 ++++++++++--------- .../database/stream/StreamStatisticsEntry.kt | 28 +++++++++--------- .../org/schabi/newpipe/player/Player.java | 3 +- 8 files changed, 87 insertions(+), 67 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/database/LocalItem.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/LocalItem.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java create mode 100644 app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.kt diff --git a/app/src/main/java/org/schabi/newpipe/database/LocalItem.java b/app/src/main/java/org/schabi/newpipe/database/LocalItem.java deleted file mode 100644 index 54b856b0653..00000000000 --- a/app/src/main/java/org/schabi/newpipe/database/LocalItem.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.schabi.newpipe.database; - -public interface LocalItem { - LocalItemType getLocalItemType(); - - enum LocalItemType { - PLAYLIST_LOCAL_ITEM, - PLAYLIST_REMOTE_ITEM, - - PLAYLIST_STREAM_ITEM, - STATISTIC_STREAM_ITEM, - } -} diff --git a/app/src/main/java/org/schabi/newpipe/database/LocalItem.kt b/app/src/main/java/org/schabi/newpipe/database/LocalItem.kt new file mode 100644 index 00000000000..87084cd51d3 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/LocalItem.kt @@ -0,0 +1,18 @@ +package org.schabi.newpipe.database + +/** + * Represents a generic item that can be stored locally. This can be a playlist, a stream, etc. + */ +interface LocalItem { + /** + * The type of local item. Can be null if the type is unknown or not applicable. + */ + val localItemType: LocalItemType? + + enum class LocalItemType { + PLAYLIST_LOCAL_ITEM, + PLAYLIST_REMOTE_ITEM, + PLAYLIST_STREAM_ITEM, + STATISTIC_STREAM_ITEM, + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntry.kt b/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntry.kt index 1eb299890e6..27fc429f1b9 100644 --- a/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntry.kt +++ b/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntry.kt @@ -30,18 +30,16 @@ data class StreamHistoryEntry( accessDate.isEqual(other.accessDate) } - fun toStreamInfoItem(): StreamInfoItem { - val item = StreamInfoItem( + fun toStreamInfoItem(): StreamInfoItem = + StreamInfoItem( streamEntity.serviceId, streamEntity.url, streamEntity.title, - streamEntity.streamType - ) - item.duration = streamEntity.duration - item.uploaderName = streamEntity.uploader - item.uploaderUrl = streamEntity.uploaderUrl - item.thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl) - - return item - } + streamEntity.streamType, + ).apply { + duration = streamEntity.duration + uploaderName = streamEntity.uploader + uploaderUrl = streamEntity.uploaderUrl + thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl) + } } diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java deleted file mode 100644 index a974a09d0da..00000000000 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.schabi.newpipe.database.playlist; - -import org.schabi.newpipe.database.LocalItem; - -public interface PlaylistLocalItem extends LocalItem { - String getOrderingName(); - - long getDisplayIndex(); - - long getUid(); - - void setDisplayIndex(long displayIndex); - - String getThumbnailUrl(); -} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.kt b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.kt new file mode 100644 index 00000000000..22d57572c24 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.kt @@ -0,0 +1,28 @@ +package org.schabi.newpipe.database.playlist + +import org.schabi.newpipe.database.LocalItem + +/** + * Represents a playlist item stored locally. + */ +interface PlaylistLocalItem : LocalItem { + /** + * The name used for ordering this item within the playlist. Can be null. + */ + val orderingName: String? + + /** + * The index used to display this item within the playlist. + */ + var displayIndex: Long + + /** + * The unique identifier for this playlist item. + */ + val uid: Long + + /** + * The URL of the thumbnail image for this playlist item. Can be null. + */ + val thumbnailUrl: String? +} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.kt b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.kt index 1d74c6d31dc..1b40d223fa1 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.kt +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.kt @@ -22,19 +22,20 @@ data class PlaylistStreamEntry( @ColumnInfo(name = PlaylistStreamEntity.JOIN_INDEX) val joinIndex: Int ) : LocalItem { - @Throws(IllegalArgumentException::class) - fun toStreamInfoItem(): StreamInfoItem { - val item = StreamInfoItem(streamEntity.serviceId, streamEntity.url, streamEntity.title, streamEntity.streamType) - item.duration = streamEntity.duration - item.uploaderName = streamEntity.uploader - item.uploaderUrl = streamEntity.uploaderUrl - item.thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl) - - return item - } - - override fun getLocalItemType(): LocalItem.LocalItemType { - return LocalItem.LocalItemType.PLAYLIST_STREAM_ITEM - } + fun toStreamInfoItem() = + StreamInfoItem( + streamEntity.serviceId, + streamEntity.url, + streamEntity.title, + streamEntity.streamType, + ).apply { + duration = streamEntity.duration + uploaderName = streamEntity.uploader + uploaderUrl = streamEntity.uploaderUrl + thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl) + } + + override val localItemType: LocalItem.LocalItemType + get() = LocalItem.LocalItemType.PLAYLIST_STREAM_ITEM } diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.kt b/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.kt index 1f3654e7ae4..60c913d1190 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.kt +++ b/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.kt @@ -26,19 +26,21 @@ class StreamStatisticsEntry( @ColumnInfo(name = STREAM_WATCH_COUNT) val watchCount: Long ) : LocalItem { - fun toStreamInfoItem(): StreamInfoItem { - val item = StreamInfoItem(streamEntity.serviceId, streamEntity.url, streamEntity.title, streamEntity.streamType) - item.duration = streamEntity.duration - item.uploaderName = streamEntity.uploader - item.uploaderUrl = streamEntity.uploaderUrl - item.thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl) - - return item - } - - override fun getLocalItemType(): LocalItem.LocalItemType { - return LocalItem.LocalItemType.STATISTIC_STREAM_ITEM - } + fun toStreamInfoItem() = + StreamInfoItem( + streamEntity.serviceId, + streamEntity.url, + streamEntity.title, + streamEntity.streamType, + ).apply { + duration = streamEntity.duration + uploaderName = streamEntity.uploader + uploaderUrl = streamEntity.uploaderUrl + thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl) + } + + override val localItemType: LocalItem.LocalItemType + get() = LocalItem.LocalItemType.STATISTIC_STREAM_ITEM companion object { const val STREAM_LATEST_DATE = "latestAccess" diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index 7f5710b7e9a..95692cbed6c 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -45,6 +45,7 @@ import static org.schabi.newpipe.util.ListHelper.getPopupResolutionIndex; import static org.schabi.newpipe.util.ListHelper.getResolutionIndex; import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; +import static java.util.Objects.requireNonNull; import static java.util.concurrent.TimeUnit.MILLISECONDS; import android.content.BroadcastReceiver; @@ -417,7 +418,7 @@ public void handleIntent(@NonNull final Intent intent) { } if (playQueue.getIndex() != newQueue.getIndex()) { simpleExoPlayer.seekTo(newQueue.getIndex(), - newQueue.getItem().getRecoveryPosition()); + requireNonNull(newQueue.getItem()).getRecoveryPosition()); } simpleExoPlayer.setPlayWhenReady(playWhenReady); From 83c71149ff64815df9967af7c39060a397cc5d86 Mon Sep 17 00:00:00 2001 From: Profpatsch Date: Wed, 5 Feb 2025 17:25:07 +0100 Subject: [PATCH 20/31] PlayerService: convert to Kotlin (mechanical) --- .../schabi/newpipe/player/PlayerService.java | 256 ------------------ .../schabi/newpipe/player/PlayerService.kt | 248 +++++++++++++++++ 2 files changed, 248 insertions(+), 256 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/player/PlayerService.java create mode 100644 app/src/main/java/org/schabi/newpipe/player/PlayerService.kt diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayerService.java b/app/src/main/java/org/schabi/newpipe/player/PlayerService.java deleted file mode 100644 index e088290c921..00000000000 --- a/app/src/main/java/org/schabi/newpipe/player/PlayerService.java +++ /dev/null @@ -1,256 +0,0 @@ -/* - * Copyright 2017 Mauricio Colli - * Part of NewPipe - * - * License: GPL-3.0+ - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.schabi.newpipe.player; - -import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; - -import android.content.Context; -import android.content.Intent; -import android.os.Binder; -import android.os.Bundle; -import android.os.IBinder; -import android.support.v4.media.MediaBrowserCompat.MediaItem; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.media.MediaBrowserServiceCompat; - -import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector; - -import org.schabi.newpipe.player.mediabrowser.MediaBrowserConnector; -import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi; -import org.schabi.newpipe.player.notification.NotificationPlayerUi; -import org.schabi.newpipe.util.ThemeHelper; - -import java.lang.ref.WeakReference; - -import java.util.List; -import java.util.Objects; - -import io.reactivex.rxjava3.disposables.CompositeDisposable; -import io.reactivex.rxjava3.disposables.Disposable; - -/** - * One service for all players. - */ -public final class PlayerService extends MediaBrowserServiceCompat { - private static final String TAG = PlayerService.class.getSimpleName(); - private static final boolean DEBUG = Player.DEBUG; - - @Nullable - private Player player; - - private final IBinder mBinder = new PlayerService.LocalBinder(this); - - - private MediaBrowserConnector mediaBrowserConnector; - private final CompositeDisposable compositeDisposableLoadChildren = new CompositeDisposable(); - - - /*////////////////////////////////////////////////////////////////////////// - // Service's LifeCycle - //////////////////////////////////////////////////////////////////////////*/ - - @Override - public void onCreate() { - super.onCreate(); - - if (DEBUG) { - Log.d(TAG, "onCreate() called"); - } - assureCorrectAppLanguage(this); - ThemeHelper.setTheme(this); - - mediaBrowserConnector = new MediaBrowserConnector(this); - } - - private void initializePlayerIfNeeded() { - if (player == null) { - player = new Player(this); - /* - Create the player notification and start immediately the service in foreground, - otherwise if nothing is played or initializing the player and its components (especially - loading stream metadata) takes a lot of time, the app would crash on Android 8+ as the - service would never be put in the foreground while we said to the system we would do so - */ - player.UIs().get(NotificationPlayerUi.class) - .ifPresent(NotificationPlayerUi::createNotificationAndStartForeground); - } - } - - // Suppress Sonar warning to not always return the same value, as we need to do some actions - // before returning - @SuppressWarnings("squid:S3516") - @Override - public int onStartCommand(final Intent intent, final int flags, final int startId) { - if (DEBUG) { - Log.d(TAG, "onStartCommand() called with: intent = [" + intent - + "], flags = [" + flags + "], startId = [" + startId + "]"); - } - - /* - Be sure that the player notification is set and the service is started in foreground, - otherwise, the app may crash on Android 8+ as the service would never be put in the - foreground while we said to the system we would do so - The service is always requested to be started in foreground, so always creating a - notification if there is no one already and starting the service in foreground should - not create any issues - If the service is already started in foreground, requesting it to be started shouldn't - do anything - */ - if (player != null) { - player.UIs().get(NotificationPlayerUi.class) - .ifPresent(NotificationPlayerUi::createNotificationAndStartForeground); - } - - if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction()) - && (player == null || player.getPlayQueue() == null)) { - /* - No need to process media button's actions if the player is not working, otherwise - the player service would strangely start with nothing to play - Stop the service in this case, which will be removed from the foreground and its - notification cancelled in its destruction - */ - stopSelf(); - return START_NOT_STICKY; - } - - initializePlayerIfNeeded(); - Objects.requireNonNull(player).handleIntent(intent); - player.UIs().get(MediaSessionPlayerUi.class) - .ifPresent(ui -> ui.handleMediaButtonIntent(intent)); - - return START_NOT_STICKY; - } - - public void stopForImmediateReusing() { - if (DEBUG) { - Log.d(TAG, "stopForImmediateReusing() called"); - } - - if (player != null && !player.exoPlayerIsNull()) { - // Releases wifi & cpu, disables keepScreenOn, etc. - // We can't just pause the player here because it will make transition - // from one stream to a new stream not smooth - player.smoothStopForImmediateReusing(); - } - } - - @Override - public void onTaskRemoved(final Intent rootIntent) { - super.onTaskRemoved(rootIntent); - if (player != null && !player.videoPlayerSelected()) { - return; - } - onDestroy(); - // Unload from memory completely - Runtime.getRuntime().halt(0); - } - - @Override - public void onDestroy() { - if (DEBUG) { - Log.d(TAG, "destroy() called"); - } - - cleanup(); - - if (mediaBrowserConnector != null) { - mediaBrowserConnector.release(); - mediaBrowserConnector = null; - } - - compositeDisposableLoadChildren.clear(); - } - - private void cleanup() { - if (player != null) { - player.destroy(); - player = null; - } - } - - public void stopService() { - cleanup(); - stopSelf(); - } - - @Override - protected void attachBaseContext(final Context base) { - super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base)); - } - - @Override - public IBinder onBind(@NonNull final Intent intent) { - if (SERVICE_INTERFACE.equals(intent.getAction())) { - return super.onBind(intent); - } - return mBinder; - } - - @NonNull - public MediaSessionConnector getSessionConnector() { - return mediaBrowserConnector.getSessionConnector(); - } - - // MediaBrowserServiceCompat methods - @Nullable - @Override - public BrowserRoot onGetRoot(@NonNull final String clientPackageName, - final int clientUid, - @Nullable final Bundle rootHints) { - return mediaBrowserConnector.onGetRoot(clientPackageName, clientUid, rootHints); - } - - @Override - public void onLoadChildren(@NonNull final String parentId, - @NonNull final Result> result) { - result.detach(); - final Disposable disposable = mediaBrowserConnector.onLoadChildren(parentId) - .subscribe(result::sendResult); - compositeDisposableLoadChildren.add(disposable); - } - - @Override - public void onSearch(@NonNull final String query, - final Bundle extras, - @NonNull final Result> result) { - mediaBrowserConnector.onSearch(query, result); - } - - public static final class LocalBinder extends Binder { - private final WeakReference playerService; - - LocalBinder(final PlayerService playerService) { - this.playerService = new WeakReference<>(playerService); - } - - @Nullable - public PlayerService getService() { - return playerService.get(); - } - - @Nullable - public Player getPlayer() { - return playerService.get().player; - } - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayerService.kt b/app/src/main/java/org/schabi/newpipe/player/PlayerService.kt new file mode 100644 index 00000000000..cf1d2fca7c4 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/PlayerService.kt @@ -0,0 +1,248 @@ +/* + * Copyright 2017 Mauricio Colli + * Part of NewPipe + * + * License: GPL-3.0+ + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.schabi.newpipe.player + +import android.content.Context +import android.content.Intent +import android.os.Binder +import android.os.Bundle +import android.os.IBinder +import android.support.v4.media.MediaBrowserCompat +import android.util.Log +import androidx.media.MediaBrowserServiceCompat +import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector +import io.reactivex.rxjava3.disposables.CompositeDisposable +import org.schabi.newpipe.player.PlayerService.LocalBinder +import org.schabi.newpipe.player.mediabrowser.MediaBrowserConnector +import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi +import org.schabi.newpipe.player.notification.NotificationPlayerUi +import org.schabi.newpipe.util.Localization +import org.schabi.newpipe.util.ThemeHelper +import java.lang.ref.WeakReference +import java.util.Objects +import java.util.function.Consumer + +/** + * One service for all players. + */ +class PlayerService : MediaBrowserServiceCompat() { + private var player: Player? = null + + private val mBinder: IBinder = LocalBinder(this) + + private var mediaBrowserConnector: MediaBrowserConnector? = null + private val compositeDisposableLoadChildren = CompositeDisposable() + + /*////////////////////////////////////////////////////////////////////////// + // Service's LifeCycle + ////////////////////////////////////////////////////////////////////////// */ + override fun onCreate() { + super.onCreate() + + if (DEBUG) { + Log.d(TAG, "onCreate() called") + } + Localization.assureCorrectAppLanguage(this) + ThemeHelper.setTheme(this) + + mediaBrowserConnector = MediaBrowserConnector(this) + } + + private fun initializePlayerIfNeeded() { + if (player == null) { + player = Player(this) + /* + Create the player notification and start immediately the service in foreground, + otherwise if nothing is played or initializing the player and its components (especially + loading stream metadata) takes a lot of time, the app would crash on Android 8+ as the + service would never be put in the foreground while we said to the system we would do so + */ + player!!.UIs().get(NotificationPlayerUi::class.java) + .ifPresent(Consumer { obj: NotificationPlayerUi? -> obj!!.createNotificationAndStartForeground() }) + } + } + + // Suppress Sonar warning to not always return the same value, as we need to do some actions + // before returning + override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { + if (DEBUG) { + Log.d( + TAG, + ( + "onStartCommand() called with: intent = [" + intent + + "], flags = [" + flags + "], startId = [" + startId + "]" + ) + ) + } + + /* + Be sure that the player notification is set and the service is started in foreground, + otherwise, the app may crash on Android 8+ as the service would never be put in the + foreground while we said to the system we would do so + The service is always requested to be started in foreground, so always creating a + notification if there is no one already and starting the service in foreground should + not create any issues + If the service is already started in foreground, requesting it to be started shouldn't + do anything + */ + if (player != null) { + player!!.UIs().get(NotificationPlayerUi::class.java) + .ifPresent(Consumer { obj: NotificationPlayerUi? -> obj!!.createNotificationAndStartForeground() }) + } + + if (Intent.ACTION_MEDIA_BUTTON == intent.getAction() && + (player == null || player!!.getPlayQueue() == null) + ) { + /* + No need to process media button's actions if the player is not working, otherwise + the player service would strangely start with nothing to play + Stop the service in this case, which will be removed from the foreground and its + notification cancelled in its destruction + */ + stopSelf() + return START_NOT_STICKY + } + + initializePlayerIfNeeded() + Objects.requireNonNull(player).handleIntent(intent) + player!!.UIs().get(MediaSessionPlayerUi::class.java) + .ifPresent(Consumer { ui: MediaSessionPlayerUi? -> ui!!.handleMediaButtonIntent(intent) }) + + return START_NOT_STICKY + } + + fun stopForImmediateReusing() { + if (DEBUG) { + Log.d(TAG, "stopForImmediateReusing() called") + } + + if (player != null && !player!!.exoPlayerIsNull()) { + // Releases wifi & cpu, disables keepScreenOn, etc. + // We can't just pause the player here because it will make transition + // from one stream to a new stream not smooth + player!!.smoothStopForImmediateReusing() + } + } + + override fun onTaskRemoved(rootIntent: Intent?) { + super.onTaskRemoved(rootIntent) + if (player != null && !player!!.videoPlayerSelected()) { + return + } + onDestroy() + // Unload from memory completely + Runtime.getRuntime().halt(0) + } + + override fun onDestroy() { + if (DEBUG) { + Log.d(TAG, "destroy() called") + } + + cleanup() + + if (mediaBrowserConnector != null) { + mediaBrowserConnector!!.release() + mediaBrowserConnector = null + } + + compositeDisposableLoadChildren.clear() + } + + private fun cleanup() { + if (player != null) { + player!!.destroy() + player = null + } + } + + fun stopService() { + cleanup() + stopSelf() + } + + override fun attachBaseContext(base: Context?) { + super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base)) + } + + override fun onBind(intent: Intent): IBinder? { + if (SERVICE_INTERFACE == intent.getAction()) { + return super.onBind(intent) + } + return mBinder + } + + fun getSessionConnector(): MediaSessionConnector { + return mediaBrowserConnector!!.getSessionConnector() + } + + // MediaBrowserServiceCompat methods + override fun onGetRoot( + clientPackageName: String, + clientUid: Int, + rootHints: Bundle? + ): BrowserRoot? { + return mediaBrowserConnector!!.onGetRoot(clientPackageName, clientUid, rootHints) + } + + override fun onLoadChildren( + parentId: String, + result: Result?> + ) { + result.detach() + val disposable = mediaBrowserConnector!!.onLoadChildren(parentId) + .subscribe( + io.reactivex.rxjava3.functions.Consumer { + result.sendResult( + it + ) + } + ) + compositeDisposableLoadChildren.add(disposable) + } + + override fun onSearch( + query: String, + extras: Bundle?, + result: Result?> + ) { + mediaBrowserConnector!!.onSearch(query, result) + } + + class LocalBinder internal constructor(playerService: PlayerService?) : Binder() { + private val playerService: WeakReference + + init { + this.playerService = WeakReference(playerService) + } + + fun getService(): PlayerService? { + return playerService.get() + } + + fun getPlayer(): Player? { + return playerService.get()!!.player + } + } + + companion object { + private val TAG: String = PlayerService::class.java.getSimpleName() + private val DEBUG = Player.DEBUG + } +} From aa750d0b9a737794c6e0b95b9975afe05a8f1686 Mon Sep 17 00:00:00 2001 From: Profpatsch Date: Wed, 5 Feb 2025 17:31:31 +0100 Subject: [PATCH 21/31] MediaBrowserConnector: convert to Kotlin (mechanical) --- .../mediabrowser/MediaBrowserConnector.java | 730 --------------- .../mediabrowser/MediaBrowserConnector.kt | 855 ++++++++++++++++++ 2 files changed, 855 insertions(+), 730 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java create mode 100644 app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.kt diff --git a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java deleted file mode 100644 index e7856db067f..00000000000 --- a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java +++ /dev/null @@ -1,730 +0,0 @@ -package org.schabi.newpipe.player.mediabrowser; - -import static org.schabi.newpipe.MainActivity.DEBUG; - -import android.content.ContentResolver; -import android.content.res.Resources; -import android.net.Uri; -import android.os.Bundle; -import android.os.ResultReceiver; -import android.support.v4.media.MediaBrowserCompat.MediaItem; -import android.support.v4.media.MediaDescriptionCompat; -import android.support.v4.media.session.MediaSessionCompat; -import android.support.v4.media.session.PlaybackStateCompat; -import android.util.Log; -import android.util.Pair; - -import androidx.annotation.DrawableRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; -import androidx.media.MediaBrowserServiceCompat; -import androidx.media.utils.MediaConstants; - -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector; - -import org.schabi.newpipe.BuildConfig; -import org.schabi.newpipe.NewPipeDatabase; -import org.schabi.newpipe.R; -import org.schabi.newpipe.database.AppDatabase; -import org.schabi.newpipe.database.history.dao.StreamHistoryDAO; -import org.schabi.newpipe.database.history.model.StreamHistoryEntry; -import org.schabi.newpipe.database.playlist.PlaylistLocalItem; -import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; -import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; -import org.schabi.newpipe.error.ErrorInfo; -import org.schabi.newpipe.error.UserAction; -import org.schabi.newpipe.extractor.InfoItem; -import org.schabi.newpipe.extractor.ListInfo; -import org.schabi.newpipe.extractor.channel.ChannelInfoItem; -import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; -import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException; -import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler; -import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; -import org.schabi.newpipe.extractor.stream.StreamInfoItem; -import org.schabi.newpipe.local.bookmark.MergedPlaylistManager; -import org.schabi.newpipe.extractor.search.SearchExtractor; -import org.schabi.newpipe.extractor.search.SearchInfo; -import org.schabi.newpipe.local.playlist.LocalPlaylistManager; -import org.schabi.newpipe.local.playlist.RemotePlaylistManager; -import org.schabi.newpipe.player.PlayerService; -import org.schabi.newpipe.player.playqueue.ChannelTabPlayQueue; -import org.schabi.newpipe.player.playqueue.PlayQueue; -import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue; -import org.schabi.newpipe.player.playqueue.SinglePlayQueue; -import org.schabi.newpipe.util.ChannelTabHelper; -import org.schabi.newpipe.util.ExtractorHelper; -import org.schabi.newpipe.util.NavigationHelper; -import org.schabi.newpipe.util.ServiceHelper; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; -import io.reactivex.rxjava3.core.Flowable; -import io.reactivex.rxjava3.core.Single; -import io.reactivex.rxjava3.disposables.Disposable; - -import io.reactivex.rxjava3.core.SingleSource; -import io.reactivex.rxjava3.schedulers.Schedulers; - -public class MediaBrowserConnector implements MediaSessionConnector.PlaybackPreparer { - - private static final String TAG = MediaBrowserConnector.class.getSimpleName(); - - @NonNull - private final PlayerService playerService; - @NonNull - private final MediaSessionConnector sessionConnector; - @NonNull - private final MediaSessionCompat mediaSession; - - private AppDatabase database; - private LocalPlaylistManager localPlaylistManager; - private RemotePlaylistManager remotePlaylistManager; - private Disposable prepareOrPlayDisposable; - private Disposable searchDisposable; - - public MediaBrowserConnector(@NonNull final PlayerService playerService) { - this.playerService = playerService; - mediaSession = new MediaSessionCompat(playerService, TAG); - sessionConnector = new MediaSessionConnector(mediaSession); - sessionConnector.setMetadataDeduplicationEnabled(true); - sessionConnector.setPlaybackPreparer(this); - playerService.setSessionToken(mediaSession.getSessionToken()); - - setupBookmarksNotifications(); - } - - @NonNull - public MediaSessionConnector getSessionConnector() { - return sessionConnector; - } - - public void release() { - disposePrepareOrPlayCommands(); - disposeBookmarksNotifications(); - mediaSession.release(); - } - - @NonNull - private static final String ID_AUTHORITY = BuildConfig.APPLICATION_ID; - @NonNull - private static final String ID_ROOT = "//" + ID_AUTHORITY; - @NonNull - private static final String ID_BOOKMARKS = "playlists"; - @NonNull - private static final String ID_HISTORY = "history"; - @NonNull - private static final String ID_INFO_ITEM = "item"; - - @NonNull - private static final String ID_LOCAL = "local"; - @NonNull - private static final String ID_REMOTE = "remote"; - @NonNull - private static final String ID_URL = "url"; - @NonNull - private static final String ID_STREAM = "stream"; - @NonNull - private static final String ID_PLAYLIST = "playlist"; - @NonNull - private static final String ID_CHANNEL = "channel"; - - @NonNull - private MediaItem createRootMediaItem(@Nullable final String mediaId, - final String folderName, - @DrawableRes final int iconResId) { - final var builder = new MediaDescriptionCompat.Builder(); - builder.setMediaId(mediaId); - builder.setTitle(folderName); - final Resources resources = playerService.getResources(); - builder.setIconUri(new Uri.Builder() - .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) - .authority(resources.getResourcePackageName(iconResId)) - .appendPath(resources.getResourceTypeName(iconResId)) - .appendPath(resources.getResourceEntryName(iconResId)) - .build()); - - final Bundle extras = new Bundle(); - extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, - playerService.getString(R.string.app_name)); - builder.setExtras(extras); - return new MediaItem(builder.build(), MediaItem.FLAG_BROWSABLE); - } - - @NonNull - private MediaItem createPlaylistMediaItem(@NonNull final PlaylistLocalItem playlist) { - final var builder = new MediaDescriptionCompat.Builder(); - final boolean remote = playlist instanceof PlaylistRemoteEntity; - builder.setMediaId(createMediaIdForInfoItem(remote, playlist.getUid())) - .setTitle(playlist.getOrderingName()) - .setIconUri(Uri.parse(playlist.getThumbnailUrl())); - - final Bundle extras = new Bundle(); - extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, - playerService.getResources().getString(R.string.tab_bookmarks)); - builder.setExtras(extras); - return new MediaItem(builder.build(), MediaItem.FLAG_BROWSABLE); - } - - private MediaItem createInfoItemMediaItem(@NonNull final InfoItem item) { - final var builder = new MediaDescriptionCompat.Builder(); - builder.setMediaId(createMediaIdForInfoItem(item)) - .setTitle(item.getName()); - - switch (item.getInfoType()) { - case STREAM: - builder.setSubtitle(((StreamInfoItem) item).getUploaderName()); - break; - case PLAYLIST: - builder.setSubtitle(((PlaylistInfoItem) item).getUploaderName()); - break; - case CHANNEL: - builder.setSubtitle(((ChannelInfoItem) item).getDescription()); - break; - default: - break; - } - final var thumbnails = item.getThumbnails(); - if (!thumbnails.isEmpty()) { - builder.setIconUri(Uri.parse(thumbnails.get(0).getUrl())); - } - return new MediaItem(builder.build(), MediaItem.FLAG_PLAYABLE); - } - - @NonNull - private Uri.Builder buildMediaId() { - return new Uri.Builder().authority(ID_AUTHORITY); - } - - @NonNull - private Uri.Builder buildPlaylistMediaId(final String playlistType) { - return buildMediaId() - .appendPath(ID_BOOKMARKS) - .appendPath(playlistType); - } - - @NonNull - private Uri.Builder buildLocalPlaylistItemMediaId(final boolean remote, final long playlistId) { - return buildPlaylistMediaId(remote ? ID_REMOTE : ID_LOCAL) - .appendPath(Long.toString(playlistId)); - } - - private static String infoItemTypeToString(final InfoItem.InfoType type) { - return switch (type) { - case STREAM -> ID_STREAM; - case PLAYLIST -> ID_PLAYLIST; - case CHANNEL -> ID_CHANNEL; - default -> - throw new IllegalStateException("Unexpected value: " + type); - }; - } - - private static InfoItem.InfoType infoItemTypeFromString(final String type) { - return switch (type) { - case ID_STREAM -> InfoItem.InfoType.STREAM; - case ID_PLAYLIST -> InfoItem.InfoType.PLAYLIST; - case ID_CHANNEL -> InfoItem.InfoType.CHANNEL; - default -> - throw new IllegalStateException("Unexpected value: " + type); - }; - } - - @NonNull - private Uri.Builder buildInfoItemMediaId(@NonNull final InfoItem item) { - return buildMediaId() - .appendPath(ID_INFO_ITEM) - .appendPath(infoItemTypeToString(item.getInfoType())) - .appendPath(Integer.toString(item.getServiceId())) - .appendQueryParameter(ID_URL, item.getUrl()); - } - - @NonNull - private String createMediaIdForInfoItem(final boolean remote, final long playlistId) { - return buildLocalPlaylistItemMediaId(remote, playlistId) - .build().toString(); - } - - @NonNull - private MediaItem createLocalPlaylistStreamMediaItem(final long playlistId, - @NonNull final PlaylistStreamEntry item, - final int index) { - final var builder = new MediaDescriptionCompat.Builder(); - builder.setMediaId(createMediaIdForPlaylistIndex(false, playlistId, index)) - .setTitle(item.getStreamEntity().getTitle()) - .setSubtitle(item.getStreamEntity().getUploader()) - .setIconUri(Uri.parse(item.getStreamEntity().getThumbnailUrl())); - - return new MediaItem(builder.build(), MediaItem.FLAG_PLAYABLE); - } - - @NonNull - private MediaItem createRemotePlaylistStreamMediaItem(final long playlistId, - @NonNull final StreamInfoItem item, - final int index) { - final var builder = new MediaDescriptionCompat.Builder(); - builder.setMediaId(createMediaIdForPlaylistIndex(true, playlistId, index)) - .setTitle(item.getName()) - .setSubtitle(item.getUploaderName()); - final var thumbnails = item.getThumbnails(); - if (!thumbnails.isEmpty()) { - builder.setIconUri(Uri.parse(thumbnails.get(0).getUrl())); - } - - return new MediaItem(builder.build(), MediaItem.FLAG_PLAYABLE); - } - - @NonNull - private String createMediaIdForPlaylistIndex(final boolean remote, final long playlistId, - final int index) { - return buildLocalPlaylistItemMediaId(remote, playlistId) - .appendPath(Integer.toString(index)) - .build().toString(); - } - - @NonNull - private String createMediaIdForInfoItem(@NonNull final InfoItem item) { - return buildInfoItemMediaId(item).build().toString(); - } - - @Nullable - public MediaBrowserServiceCompat.BrowserRoot onGetRoot(@NonNull final String clientPackageName, - final int clientUid, - @Nullable final Bundle rootHints) { - if (DEBUG) { - Log.d(TAG, String.format("MediaBrowserService.onGetRoot(%s, %s, %s)", - clientPackageName, clientUid, rootHints)); - } - - final Bundle extras = new Bundle(); - extras.putBoolean( - MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, true); - return new MediaBrowserServiceCompat.BrowserRoot(ID_ROOT, extras); - } - - public Single> onLoadChildren(@NonNull final String parentId) { - if (DEBUG) { - Log.d(TAG, String.format("MediaBrowserService.onLoadChildren(%s)", parentId)); - } - - - try { - final Uri parentIdUri = Uri.parse(parentId); - final List path = new ArrayList<>(parentIdUri.getPathSegments()); - - if (path.isEmpty()) { - final List mediaItems = new ArrayList<>(); - mediaItems.add( - createRootMediaItem(ID_BOOKMARKS, - playerService.getResources().getString( - R.string.tab_bookmarks_short), - R.drawable.ic_bookmark_white)); - mediaItems.add( - createRootMediaItem(ID_HISTORY, - playerService.getResources().getString(R.string.action_history), - R.drawable.ic_history_white)); - return Single.just(mediaItems); - } - - final String uriType = path.get(0); - path.remove(0); - - switch (uriType) { - case ID_BOOKMARKS: - if (path.isEmpty()) { - return populateBookmarks(); - } - if (path.size() == 2) { - final String localOrRemote = path.get(0); - final long playlistId = Long.parseLong(path.get(1)); - if (localOrRemote.equals(ID_LOCAL)) { - return populateLocalPlaylist(playlistId); - } else if (localOrRemote.equals(ID_REMOTE)) { - return populateRemotePlaylist(playlistId); - } - } - Log.w(TAG, "Unknown playlist URI: " + parentId); - throw parseError(); - case ID_HISTORY: - return populateHistory(); - default: - throw parseError(); - } - } catch (final ContentNotAvailableException e) { - return Single.error(e); - } - } - - private Single> populateHistory() { - final StreamHistoryDAO streamHistory = getDatabase().streamHistoryDAO(); - final var history = streamHistory.getHistory().firstOrError(); - return history.map(items -> items.stream() - .map(this::createHistoryMediaItem) - .collect(Collectors.toList())); - } - - @NonNull - private MediaItem createHistoryMediaItem(@NonNull final StreamHistoryEntry streamHistoryEntry) { - final var builder = new MediaDescriptionCompat.Builder(); - final var mediaId = buildMediaId() - .appendPath(ID_HISTORY) - .appendPath(Long.toString(streamHistoryEntry.getStreamId())) - .build().toString(); - builder.setMediaId(mediaId) - .setTitle(streamHistoryEntry.getStreamEntity().getTitle()) - .setSubtitle(streamHistoryEntry.getStreamEntity().getUploader()) - .setIconUri(Uri.parse(streamHistoryEntry.getStreamEntity().getThumbnailUrl())); - - return new MediaItem(builder.build(), MediaItem.FLAG_PLAYABLE); - } - - private AppDatabase getDatabase() { - if (database == null) { - database = NewPipeDatabase.getInstance(playerService); - } - return database; - } - - private Flowable> getPlaylists() { - if (localPlaylistManager == null) { - localPlaylistManager = new LocalPlaylistManager(getDatabase()); - } - if (remotePlaylistManager == null) { - remotePlaylistManager = new RemotePlaylistManager(getDatabase()); - } - return MergedPlaylistManager.getMergedOrderedPlaylists(localPlaylistManager, - remotePlaylistManager); - } - - @Nullable Disposable bookmarksNotificationsDisposable; - - private void setupBookmarksNotifications() { - bookmarksNotificationsDisposable = getPlaylists().subscribe( - playlistMetadataEntries -> playerService.notifyChildrenChanged(ID_BOOKMARKS)); - } - - private void disposeBookmarksNotifications() { - if (bookmarksNotificationsDisposable != null) { - bookmarksNotificationsDisposable.dispose(); - bookmarksNotificationsDisposable = null; - } - } - - // Suppress Sonar warning replace list collection by Stream.toList call, as this method is only - // available in Android API 34 and not currently available with desugaring - @SuppressWarnings("squid:S6204") - private Single> populateBookmarks() { - final var playlists = getPlaylists().firstOrError(); - return playlists.map(playlist -> playlist.stream() - .map(this::createPlaylistMediaItem) - .collect(Collectors.toList())); - } - - private Single> populateLocalPlaylist(final long playlistId) { - final var playlist = localPlaylistManager.getPlaylistStreams(playlistId).firstOrError(); - return playlist.map(items -> { - final List results = new ArrayList<>(); - int index = 0; - for (final PlaylistStreamEntry item : items) { - results.add(createLocalPlaylistStreamMediaItem(playlistId, item, index)); - ++index; - } - return results; - }); - } - - private Single>> getRemotePlaylist(final long playlistId) { - final var playlistFlow = remotePlaylistManager.getPlaylist(playlistId).firstOrError(); - return playlistFlow.flatMap(item -> { - final var playlist = item.get(0); - final var playlistInfo = ExtractorHelper.getPlaylistInfo(playlist.getServiceId(), - playlist.getUrl(), false); - return playlistInfo.flatMap(info -> { - final var infoItemsPage = info.getRelatedItems(); - - if (!info.getErrors().isEmpty()) { - final List errors = new ArrayList<>(info.getErrors()); - - errors.removeIf(ContentNotSupportedException.class::isInstance); - - if (!errors.isEmpty()) { - return Single.error(errors.get(0)); - } - } - - return Single.just(IntStream.range(0, infoItemsPage.size()) - .mapToObj(i -> Pair.create(infoItemsPage.get(i), i)) - .toList()); - }); - }); - } - - private Single> populateRemotePlaylist(final long playlistId) { - return getRemotePlaylist(playlistId).map(items -> - items.stream().map(pair -> - createRemotePlaylistStreamMediaItem(playlistId, pair.first, pair.second) - ).toList() - ); - } - - private void playbackError(@StringRes final int resId, final int code) { - playerService.stopForImmediateReusing(); - sessionConnector.setCustomErrorMessage(playerService.getString(resId), code); - } - - private void playbackError(@NonNull final ErrorInfo errorInfo) { - playbackError(errorInfo.getMessageStringId(), PlaybackStateCompat.ERROR_CODE_APP_ERROR); - } - - private Single extractLocalPlayQueue(final long playlistId, final int index) { - return localPlaylistManager.getPlaylistStreams(playlistId) - .firstOrError() - .map(items -> { - final List infoItems = items.stream() - .map(PlaylistStreamEntry::toStreamInfoItem) - .collect(Collectors.toList()); - return new SinglePlayQueue(infoItems, index); - }); - } - - private Single extractRemotePlayQueue(final long playlistId, final int index) { - return getRemotePlaylist(playlistId).map(items -> { - final var infoItems = items.stream().map(pair -> pair.first).toList(); - return new SinglePlayQueue(infoItems, index); - }); - } - - private static ContentNotAvailableException parseError() { - return new ContentNotAvailableException("Failed to parse media ID"); - } - - private Single extractPlayQueueFromMediaId(final String mediaId) { - try { - final Uri mediaIdUri = Uri.parse(mediaId); - final List path = new ArrayList<>(mediaIdUri.getPathSegments()); - - if (path.isEmpty()) { - throw parseError(); - } - - final String uriType = path.get(0); - path.remove(0); - - return switch (uriType) { - case ID_BOOKMARKS -> extractPlayQueueFromPlaylistMediaId(path, - mediaIdUri.getQueryParameter(ID_URL)); - case ID_HISTORY -> extractPlayQueueFromHistoryMediaId(path); - case ID_INFO_ITEM -> extractPlayQueueFromInfoItemMediaId(path, - mediaIdUri.getQueryParameter(ID_URL)); - default -> throw parseError(); - }; - } catch (final ContentNotAvailableException e) { - return Single.error(e); - } - } - - private Single - extractPlayQueueFromPlaylistMediaId( - @NonNull final List path, - @Nullable final String url) throws ContentNotAvailableException { - if (path.isEmpty()) { - throw parseError(); - } - - final String playlistType = path.get(0); - path.remove(0); - - switch (playlistType) { - case ID_LOCAL, ID_REMOTE: - if (path.size() != 2) { - throw parseError(); - } - final long playlistId = Long.parseLong(path.get(0)); - final int index = Integer.parseInt(path.get(1)); - return playlistType.equals(ID_LOCAL) - ? extractLocalPlayQueue(playlistId, index) - : extractRemotePlayQueue(playlistId, index); - case ID_URL: - if (path.size() != 1) { - throw parseError(); - } - - final int serviceId = Integer.parseInt(path.get(0)); - return ExtractorHelper.getPlaylistInfo(serviceId, url, false) - .map(PlaylistPlayQueue::new); - default: - throw parseError(); - } - } - - private Single extractPlayQueueFromHistoryMediaId( - final List path) throws ContentNotAvailableException { - if (path.size() != 1) { - throw parseError(); - } - - final long streamId = Long.parseLong(path.get(0)); - return getDatabase().streamHistoryDAO().getHistory() - .firstOrError() - .map(items -> { - final List infoItems = items.stream() - .filter(it -> it.getStreamId() == streamId) - .map(StreamHistoryEntry::toStreamInfoItem) - .collect(Collectors.toList()); - return new SinglePlayQueue(infoItems, 0); - }); - } - - private static Single extractPlayQueueFromInfoItemMediaId( - final List path, final String url) throws ContentNotAvailableException { - if (path.size() != 2) { - throw parseError(); - } - final var infoItemType = infoItemTypeFromString(path.get(0)); - final int serviceId = Integer.parseInt(path.get(1)); - return switch (infoItemType) { - case STREAM -> ExtractorHelper.getStreamInfo(serviceId, url, false) - .map(SinglePlayQueue::new); - case PLAYLIST -> ExtractorHelper.getPlaylistInfo(serviceId, url, false) - .map(PlaylistPlayQueue::new); - case CHANNEL -> ExtractorHelper.getChannelInfo(serviceId, url, false) - .map(info -> { - final Optional playableTab = info.getTabs() - .stream() - .filter(ChannelTabHelper::isStreamsTab) - .findFirst(); - - if (playableTab.isPresent()) { - return new ChannelTabPlayQueue(serviceId, - new ListLinkHandler(playableTab.get())); - } else { - throw new ContentNotAvailableException("No streams tab found"); - } - }); - default -> throw parseError(); - }; - } - - @Override - public long getSupportedPrepareActions() { - return PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID; - } - - private void disposePrepareOrPlayCommands() { - if (prepareOrPlayDisposable != null) { - prepareOrPlayDisposable.dispose(); - prepareOrPlayDisposable = null; - } - } - - @Override - public void onPrepare(final boolean playWhenReady) { - disposePrepareOrPlayCommands(); - // No need to prepare - } - - @Override - public void onPrepareFromMediaId(@NonNull final String mediaId, - final boolean playWhenReady, - @Nullable final Bundle extras) { - if (DEBUG) { - Log.d(TAG, String.format("MediaBrowserConnector.onPrepareFromMediaId(%s, %s, %s)", - mediaId, playWhenReady, extras)); - } - - disposePrepareOrPlayCommands(); - prepareOrPlayDisposable = extractPlayQueueFromMediaId(mediaId) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - playQueue -> { - sessionConnector.setCustomErrorMessage(null); - NavigationHelper.playOnBackgroundPlayer(playerService, playQueue, - playWhenReady); - }, - throwable -> playbackError(new ErrorInfo(throwable, UserAction.PLAY_STREAM, - "Failed playback of media ID [" + mediaId + "]: ")) - ); - } - - @Override - public void onPrepareFromSearch(@NonNull final String query, - final boolean playWhenReady, - @Nullable final Bundle extras) { - disposePrepareOrPlayCommands(); - playbackError(R.string.content_not_supported, - PlaybackStateCompat.ERROR_CODE_NOT_SUPPORTED); - } - - private @NonNull Single searchMusicBySongTitle(final String query) { - final var serviceId = ServiceHelper.getSelectedServiceId(playerService); - return ExtractorHelper.searchFor(serviceId, query, - new ArrayList<>(), ""); - } - - private @NonNull SingleSource> - mediaItemsFromInfoItemList(final ListInfo result) { - final List exceptions = result.getErrors(); - if (!exceptions.isEmpty() - && !(exceptions.size() == 1 - && exceptions.get(0) instanceof SearchExtractor.NothingFoundException)) { - return Single.error(exceptions.get(0)); - } - - final List items = result.getRelatedItems(); - if (items.isEmpty()) { - return Single.error(new NullPointerException("Got no search results.")); - } - try { - final List results = items.stream() - .filter(item -> - item.getInfoType() == InfoItem.InfoType.STREAM - || item.getInfoType() == InfoItem.InfoType.PLAYLIST - || item.getInfoType() == InfoItem.InfoType.CHANNEL) - .map(this::createInfoItemMediaItem).toList(); - return Single.just(results); - } catch (final Exception e) { - return Single.error(e); - } - } - - private void handleSearchError(final Throwable throwable) { - Log.e(TAG, "Search error: " + throwable); - disposePrepareOrPlayCommands(); - playbackError(R.string.content_not_supported, PlaybackStateCompat.ERROR_CODE_NOT_SUPPORTED); - } - - @Override - public void onPrepareFromUri(@NonNull final Uri uri, - final boolean playWhenReady, - @Nullable final Bundle extras) { - disposePrepareOrPlayCommands(); - playbackError(R.string.content_not_supported, PlaybackStateCompat.ERROR_CODE_NOT_SUPPORTED); - } - - @Override - public boolean onCommand(@NonNull final Player player, - @NonNull final String command, - @Nullable final Bundle extras, - @Nullable final ResultReceiver cb) { - return false; - } - - public void onSearch(@NonNull final String query, - @NonNull final MediaBrowserServiceCompat.Result> result) { - result.detach(); - if (searchDisposable != null) { - searchDisposable.dispose(); - } - searchDisposable = searchMusicBySongTitle(query) - .flatMap(this::mediaItemsFromInfoItemList) - .subscribeOn(Schedulers.io()) - .subscribe(result::sendResult, - this::handleSearchError); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.kt b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.kt new file mode 100644 index 00000000000..bcccbda48a6 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.kt @@ -0,0 +1,855 @@ +package org.schabi.newpipe.player.mediabrowser + +import android.content.ContentResolver +import android.net.Uri +import android.os.Bundle +import android.os.ResultReceiver +import android.support.v4.media.MediaBrowserCompat +import android.support.v4.media.MediaDescriptionCompat +import android.support.v4.media.session.MediaSessionCompat +import android.support.v4.media.session.PlaybackStateCompat +import android.util.Log +import android.util.Pair +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.media.MediaBrowserServiceCompat +import androidx.media.utils.MediaConstants +import com.google.android.exoplayer2.Player +import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector +import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector.PlaybackPreparer +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.core.Flowable +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.core.SingleSource +import io.reactivex.rxjava3.disposables.Disposable +import io.reactivex.rxjava3.functions.Consumer +import io.reactivex.rxjava3.functions.Function +import io.reactivex.rxjava3.schedulers.Schedulers +import org.schabi.newpipe.BuildConfig +import org.schabi.newpipe.MainActivity +import org.schabi.newpipe.NewPipeDatabase +import org.schabi.newpipe.R +import org.schabi.newpipe.database.AppDatabase +import org.schabi.newpipe.database.history.model.StreamHistoryEntry +import org.schabi.newpipe.database.playlist.PlaylistLocalItem +import org.schabi.newpipe.database.playlist.PlaylistStreamEntry +import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity +import org.schabi.newpipe.error.ErrorInfo +import org.schabi.newpipe.error.UserAction +import org.schabi.newpipe.extractor.InfoItem +import org.schabi.newpipe.extractor.InfoItem.InfoType +import org.schabi.newpipe.extractor.ListInfo +import org.schabi.newpipe.extractor.channel.ChannelInfo +import org.schabi.newpipe.extractor.channel.ChannelInfoItem +import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException +import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException +import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler +import org.schabi.newpipe.extractor.playlist.PlaylistInfo +import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem +import org.schabi.newpipe.extractor.search.SearchExtractor.NothingFoundException +import org.schabi.newpipe.extractor.search.SearchInfo +import org.schabi.newpipe.extractor.stream.StreamInfo +import org.schabi.newpipe.extractor.stream.StreamInfoItem +import org.schabi.newpipe.local.bookmark.MergedPlaylistManager +import org.schabi.newpipe.local.playlist.LocalPlaylistManager +import org.schabi.newpipe.local.playlist.RemotePlaylistManager +import org.schabi.newpipe.player.PlayerService +import org.schabi.newpipe.player.playqueue.ChannelTabPlayQueue +import org.schabi.newpipe.player.playqueue.PlayQueue +import org.schabi.newpipe.player.playqueue.SinglePlayQueue +import org.schabi.newpipe.util.ChannelTabHelper +import org.schabi.newpipe.util.ExtractorHelper +import org.schabi.newpipe.util.NavigationHelper +import org.schabi.newpipe.util.ServiceHelper +import java.lang.Exception +import java.lang.IllegalStateException +import java.lang.NullPointerException +import java.util.ArrayList +import java.util.stream.Collectors +import java.util.stream.IntStream + +class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { + private val playerService: PlayerService + private val sessionConnector: MediaSessionConnector + private val mediaSession: MediaSessionCompat + + private var database: AppDatabase? = null + private var localPlaylistManager: LocalPlaylistManager? = null + private var remotePlaylistManager: RemotePlaylistManager? = null + private var prepareOrPlayDisposable: Disposable? = null + private var searchDisposable: Disposable? = null + + fun getSessionConnector(): MediaSessionConnector { + return sessionConnector + } + + fun release() { + disposePrepareOrPlayCommands() + disposeBookmarksNotifications() + mediaSession.release() + } + + private fun createRootMediaItem( + mediaId: String?, + folderName: String?, + @DrawableRes iconResId: Int + ): MediaBrowserCompat.MediaItem { + val builder = MediaDescriptionCompat.Builder() + builder.setMediaId(mediaId) + builder.setTitle(folderName) + val resources = playerService.getResources() + builder.setIconUri( + Uri.Builder() + .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) + .authority(resources.getResourcePackageName(iconResId)) + .appendPath(resources.getResourceTypeName(iconResId)) + .appendPath(resources.getResourceEntryName(iconResId)) + .build() + ) + + val extras = Bundle() + extras.putString( + MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, + playerService.getString(R.string.app_name) + ) + builder.setExtras(extras) + return MediaBrowserCompat.MediaItem( + builder.build(), + MediaBrowserCompat.MediaItem.FLAG_BROWSABLE + ) + } + + private fun createPlaylistMediaItem(playlist: PlaylistLocalItem): MediaBrowserCompat.MediaItem { + val builder = MediaDescriptionCompat.Builder() + val remote = playlist is PlaylistRemoteEntity + builder.setMediaId(createMediaIdForInfoItem(remote, playlist.uid)) + .setTitle(playlist.orderingName) + .setIconUri(Uri.parse(playlist.thumbnailUrl)) + + val extras = Bundle() + extras.putString( + MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, + playerService.getResources().getString(R.string.tab_bookmarks) + ) + builder.setExtras(extras) + return MediaBrowserCompat.MediaItem( + builder.build(), + MediaBrowserCompat.MediaItem.FLAG_BROWSABLE + ) + } + + private fun createInfoItemMediaItem(item: InfoItem): MediaBrowserCompat.MediaItem { + val builder = MediaDescriptionCompat.Builder() + builder.setMediaId(createMediaIdForInfoItem(item)) + .setTitle(item.getName()) + + when (item.getInfoType()) { + InfoType.STREAM -> builder.setSubtitle((item as StreamInfoItem).getUploaderName()) + InfoType.PLAYLIST -> builder.setSubtitle((item as PlaylistInfoItem).getUploaderName()) + InfoType.CHANNEL -> builder.setSubtitle((item as ChannelInfoItem).getDescription()) + else -> {} + } + val thumbnails = item.getThumbnails() + if (!thumbnails.isEmpty()) { + builder.setIconUri(Uri.parse(thumbnails.get(0)!!.getUrl())) + } + return MediaBrowserCompat.MediaItem( + builder.build(), + MediaBrowserCompat.MediaItem.FLAG_PLAYABLE + ) + } + + private fun buildMediaId(): Uri.Builder { + return Uri.Builder().authority(ID_AUTHORITY) + } + + private fun buildPlaylistMediaId(playlistType: String?): Uri.Builder { + return buildMediaId() + .appendPath(ID_BOOKMARKS) + .appendPath(playlistType) + } + + private fun buildLocalPlaylistItemMediaId(remote: Boolean, playlistId: Long): Uri.Builder { + return buildPlaylistMediaId(if (remote) ID_REMOTE else ID_LOCAL) + .appendPath(playlistId.toString()) + } + + private fun buildInfoItemMediaId(item: InfoItem): Uri.Builder { + return buildMediaId() + .appendPath(ID_INFO_ITEM) + .appendPath(infoItemTypeToString(item.getInfoType())) + .appendPath(item.getServiceId().toString()) + .appendQueryParameter(ID_URL, item.getUrl()) + } + + private fun createMediaIdForInfoItem(remote: Boolean, playlistId: Long): String { + return buildLocalPlaylistItemMediaId(remote, playlistId) + .build().toString() + } + + private fun createLocalPlaylistStreamMediaItem( + playlistId: Long, + item: PlaylistStreamEntry, + index: Int + ): MediaBrowserCompat.MediaItem { + val builder = MediaDescriptionCompat.Builder() + builder.setMediaId(createMediaIdForPlaylistIndex(false, playlistId, index)) + .setTitle(item.streamEntity.title) + .setSubtitle(item.streamEntity.uploader) + .setIconUri(Uri.parse(item.streamEntity.thumbnailUrl)) + + return MediaBrowserCompat.MediaItem( + builder.build(), + MediaBrowserCompat.MediaItem.FLAG_PLAYABLE + ) + } + + private fun createRemotePlaylistStreamMediaItem( + playlistId: Long, + item: StreamInfoItem, + index: Int + ): MediaBrowserCompat.MediaItem { + val builder = MediaDescriptionCompat.Builder() + builder.setMediaId(createMediaIdForPlaylistIndex(true, playlistId, index)) + .setTitle(item.getName()) + .setSubtitle(item.getUploaderName()) + val thumbnails = item.getThumbnails() + if (!thumbnails.isEmpty()) { + builder.setIconUri(Uri.parse(thumbnails.get(0)!!.getUrl())) + } + + return MediaBrowserCompat.MediaItem( + builder.build(), + MediaBrowserCompat.MediaItem.FLAG_PLAYABLE + ) + } + + private fun createMediaIdForPlaylistIndex( + remote: Boolean, + playlistId: Long, + index: Int + ): String { + return buildLocalPlaylistItemMediaId(remote, playlistId) + .appendPath(index.toString()) + .build().toString() + } + + private fun createMediaIdForInfoItem(item: InfoItem): String { + return buildInfoItemMediaId(item).build().toString() + } + + fun onGetRoot( + clientPackageName: String, + clientUid: Int, + rootHints: Bundle? + ): MediaBrowserServiceCompat.BrowserRoot? { + if (MainActivity.DEBUG) { + Log.d( + TAG, + String.format( + "MediaBrowserService.onGetRoot(%s, %s, %s)", + clientPackageName, clientUid, rootHints + ) + ) + } + + val extras = Bundle() + extras.putBoolean( + MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, true + ) + return MediaBrowserServiceCompat.BrowserRoot(ID_ROOT, extras) + } + + fun onLoadChildren(parentId: String): Single?>? { + if (MainActivity.DEBUG) { + Log.d(TAG, String.format("MediaBrowserService.onLoadChildren(%s)", parentId)) + } + + try { + val parentIdUri = Uri.parse(parentId) + val path: MutableList = ArrayList(parentIdUri.getPathSegments()) + + if (path.isEmpty()) { + val mediaItems: MutableList = + ArrayList() + mediaItems.add( + createRootMediaItem( + ID_BOOKMARKS, + playerService.getResources().getString( + R.string.tab_bookmarks_short + ), + R.drawable.ic_bookmark_white + ) + ) + mediaItems.add( + createRootMediaItem( + ID_HISTORY, + playerService.getResources().getString(R.string.action_history), + R.drawable.ic_history_white + ) + ) + return Single.just?>(mediaItems) + } + + val uriType = path.get(0) + path.removeAt(0) + + when (uriType) { + ID_BOOKMARKS -> { + if (path.isEmpty()) { + return populateBookmarks() + } + if (path.size == 2) { + val localOrRemote = path.get(0) + val playlistId = path.get(1).toLong() + if (localOrRemote == ID_LOCAL) { + return populateLocalPlaylist(playlistId) + } else if (localOrRemote == ID_REMOTE) { + return populateRemotePlaylist(playlistId) + } + } + Log.w(TAG, "Unknown playlist URI: " + parentId) + throw parseError() + } + + ID_HISTORY -> return populateHistory() + else -> throw parseError() + } + } catch (e: ContentNotAvailableException) { + return Single.error?>(e) + } + } + + private fun populateHistory(): Single?>? { + val streamHistory = getDatabase().streamHistoryDAO() + val history = streamHistory.getHistory().firstOrError() + return history.map?>( + Function { items: MutableList? -> + items!!.stream() + .map { streamHistoryEntry: StreamHistoryEntry? -> + this.createHistoryMediaItem( + streamHistoryEntry!! + ) + } + .collect(Collectors.toList()) + } + ) + } + + private fun createHistoryMediaItem(streamHistoryEntry: StreamHistoryEntry): MediaBrowserCompat.MediaItem { + val builder = MediaDescriptionCompat.Builder() + val mediaId = buildMediaId() + .appendPath(ID_HISTORY) + .appendPath(streamHistoryEntry.streamId.toString()) + .build().toString() + builder.setMediaId(mediaId) + .setTitle(streamHistoryEntry.streamEntity.title) + .setSubtitle(streamHistoryEntry.streamEntity.uploader) + .setIconUri(Uri.parse(streamHistoryEntry.streamEntity.thumbnailUrl)) + + return MediaBrowserCompat.MediaItem( + builder.build(), + MediaBrowserCompat.MediaItem.FLAG_PLAYABLE + ) + } + + private fun getDatabase(): AppDatabase { + if (database == null) { + database = NewPipeDatabase.getInstance(playerService) + } + return database!! + } + + private fun getPlaylists(): Flowable?>? { + if (localPlaylistManager == null) { + localPlaylistManager = LocalPlaylistManager(getDatabase()) + } + if (remotePlaylistManager == null) { + remotePlaylistManager = RemotePlaylistManager(getDatabase()) + } + return MergedPlaylistManager.getMergedOrderedPlaylists( + localPlaylistManager, + remotePlaylistManager + ) + } + + var bookmarksNotificationsDisposable: Disposable? = null + + init { + this.playerService = playerService + mediaSession = MediaSessionCompat(playerService, TAG) + sessionConnector = MediaSessionConnector(mediaSession) + sessionConnector.setMetadataDeduplicationEnabled(true) + sessionConnector.setPlaybackPreparer(this) + playerService.setSessionToken(mediaSession.getSessionToken()) + + setupBookmarksNotifications() + } + + private fun setupBookmarksNotifications() { + bookmarksNotificationsDisposable = getPlaylists()!!.subscribe( + Consumer { playlistMetadataEntries: MutableList? -> + playerService.notifyChildrenChanged( + ID_BOOKMARKS + ) + } + ) + } + + private fun disposeBookmarksNotifications() { + if (bookmarksNotificationsDisposable != null) { + bookmarksNotificationsDisposable!!.dispose() + bookmarksNotificationsDisposable = null + } + } + + // Suppress Sonar warning replace list collection by Stream.toList call, as this method is only + // available in Android API 34 and not currently available with desugaring + private fun populateBookmarks(): Single?>? { + val playlists = getPlaylists()!!.firstOrError() + return playlists.map?>( + Function { playlist: MutableList? -> + playlist!!.stream() + .map { playlist: PlaylistLocalItem? -> + this.createPlaylistMediaItem( + playlist!! + ) + } + .collect(Collectors.toList()) + } + ) + } + + private fun populateLocalPlaylist(playlistId: Long): Single?>? { + val playlist = localPlaylistManager!!.getPlaylistStreams(playlistId).firstOrError() + return playlist.map?>( + Function { items: MutableList? -> + val results: MutableList = + ArrayList() + var index = 0 + for (item in items!!) { + results.add(createLocalPlaylistStreamMediaItem(playlistId, item, index)) + ++index + } + results + } + ) + } + + private fun getRemotePlaylist(playlistId: Long): Single?>?>? { + val playlistFlow = remotePlaylistManager!!.getPlaylist(playlistId).firstOrError() + return playlistFlow.flatMap?>?>( + Function { item: MutableList? -> + val playlist = item!!.get(0) + val playlistInfo = ExtractorHelper.getPlaylistInfo( + playlist.getServiceId(), + playlist.getUrl(), false + ) + playlistInfo.flatMap?>?>( + Function { info: PlaylistInfo? -> + val infoItemsPage = info!!.getRelatedItems() + if (!info.getErrors().isEmpty()) { + val errors: MutableList = ArrayList(info.getErrors()) + + errors.removeIf { obj: Throwable? -> + ContentNotSupportedException::class.java.isInstance( + obj + ) + } + + if (!errors.isEmpty()) { + return@flatMap Single.error?>?>( + errors.get(0) + ) + } + } + Single.just?>?>( + IntStream.range(0, infoItemsPage.size) + .mapToObj?>( + java.util.function.IntFunction { i: kotlin.Int -> + android.util.Pair.create( + infoItemsPage.get(i), + i + ) + } + ) + .toList()!! + ) + } + ) + } + ) + } + + private fun populateRemotePlaylist(playlistId: Long): Single?>? { + return getRemotePlaylist(playlistId)!!.map?>( + Function { items: MutableList?>? -> + items!!.stream() + .map { pair: Pair? -> + createRemotePlaylistStreamMediaItem( + playlistId, + pair!!.first!!, + pair.second!! + ) + }.toList() + } + ) + } + + private fun playbackError(@StringRes resId: Int, code: Int) { + playerService.stopForImmediateReusing() + sessionConnector.setCustomErrorMessage(playerService.getString(resId), code) + } + + private fun playbackError(errorInfo: ErrorInfo) { + playbackError(errorInfo.messageStringId, PlaybackStateCompat.ERROR_CODE_APP_ERROR) + } + + private fun extractLocalPlayQueue(playlistId: Long, index: Int): Single? { + return localPlaylistManager!!.getPlaylistStreams(playlistId) + .firstOrError() + .map( + Function { items: MutableList? -> + val infoItems = items!!.stream() + .map { obj: PlaylistStreamEntry? -> obj!!.toStreamInfoItem() } + .collect(Collectors.toList()) + SinglePlayQueue(infoItems, index) + } + ) + } + + private fun extractRemotePlayQueue(playlistId: Long, index: Int): Single? { + return getRemotePlaylist(playlistId)!!.map( + Function { items: MutableList?>? -> + val infoItems = items!!.stream() + .map { pair: Pair? -> pair!!.first } + .toList() + SinglePlayQueue(infoItems, index) + } + ) + } + + private fun extractPlayQueueFromMediaId(mediaId: String?): Single? { + try { + val mediaIdUri = Uri.parse(mediaId) + val path: MutableList = ArrayList(mediaIdUri.getPathSegments()) + + if (path.isEmpty()) { + throw parseError() + } + + val uriType: String? = path.get(0) + path.removeAt(0) + + return when (uriType) { + ID_BOOKMARKS -> extractPlayQueueFromPlaylistMediaId( + path, + mediaIdUri.getQueryParameter(ID_URL) + ) + + ID_HISTORY -> extractPlayQueueFromHistoryMediaId(path) + ID_INFO_ITEM -> extractPlayQueueFromInfoItemMediaId( + path, + mediaIdUri.getQueryParameter(ID_URL) + ) + + else -> throw parseError() + } + } catch (e: ContentNotAvailableException) { + return Single.error(e) + } + } + + @Throws(ContentNotAvailableException::class) + private fun extractPlayQueueFromPlaylistMediaId( + path: MutableList, + url: String? + ): Single? { + if (path.isEmpty()) { + throw parseError() + } + + val playlistType = path.get(0) + path.removeAt(0) + + when (playlistType) { + ID_LOCAL, ID_REMOTE -> { + if (path.size != 2) { + throw parseError() + } + val playlistId = path.get(0).toLong() + val index = path.get(1).toInt() + return if (playlistType == ID_LOCAL) + extractLocalPlayQueue(playlistId, index) + else + extractRemotePlayQueue(playlistId, index) + } + + ID_URL -> { + if (path.size != 1) { + throw parseError() + } + + val serviceId = path.get(0).toInt() + return ExtractorHelper.getPlaylistInfo(serviceId, url, false) + .map(Function { info: PlaylistInfo? -> PlaylistPlayQueue(info) }) + } + + else -> throw parseError() + } + } + + @Throws(ContentNotAvailableException::class) + private fun extractPlayQueueFromHistoryMediaId( + path: MutableList + ): Single? { + if (path.size != 1) { + throw parseError() + } + + val streamId = path.get(0).toLong() + return getDatabase().streamHistoryDAO().getHistory() + .firstOrError() + .map( + Function { items: MutableList? -> + val infoItems = items!!.stream() + .filter { it: StreamHistoryEntry? -> it!!.streamId == streamId } + .map { obj: StreamHistoryEntry? -> obj!!.toStreamInfoItem() } + .collect(Collectors.toList()) + SinglePlayQueue(infoItems, 0) + } + ) + } + + override fun getSupportedPrepareActions(): Long { + return PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID + } + + private fun disposePrepareOrPlayCommands() { + if (prepareOrPlayDisposable != null) { + prepareOrPlayDisposable!!.dispose() + prepareOrPlayDisposable = null + } + } + + override fun onPrepare(playWhenReady: Boolean) { + disposePrepareOrPlayCommands() + // No need to prepare + } + + override fun onPrepareFromMediaId( + mediaId: String, + playWhenReady: Boolean, + extras: Bundle? + ) { + if (MainActivity.DEBUG) { + Log.d( + TAG, + String.format( + "MediaBrowserConnector.onPrepareFromMediaId(%s, %s, %s)", + mediaId, playWhenReady, extras + ) + ) + } + + disposePrepareOrPlayCommands() + prepareOrPlayDisposable = extractPlayQueueFromMediaId(mediaId)!! + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + Consumer { playQueue: PlayQueue? -> + sessionConnector.setCustomErrorMessage(null) + NavigationHelper.playOnBackgroundPlayer( + playerService, playQueue, + playWhenReady + ) + }, + Consumer { throwable: Throwable? -> + playbackError( + ErrorInfo( + throwable, UserAction.PLAY_STREAM, + "Failed playback of media ID [" + mediaId + "]: " + ) + ) + } + ) + } + + override fun onPrepareFromSearch( + query: String, + playWhenReady: Boolean, + extras: Bundle? + ) { + disposePrepareOrPlayCommands() + playbackError( + R.string.content_not_supported, + PlaybackStateCompat.ERROR_CODE_NOT_SUPPORTED + ) + } + + private fun searchMusicBySongTitle(query: String?): Single { + val serviceId = ServiceHelper.getSelectedServiceId(playerService) + return ExtractorHelper.searchFor( + serviceId, query, + ArrayList(), "" + ) + } + + private fun mediaItemsFromInfoItemList(result: ListInfo): SingleSource?> { + val exceptions = result.getErrors() + if (!exceptions.isEmpty() && + !( + exceptions.size == 1 && + exceptions.get(0) is NothingFoundException + ) + ) { + return Single.error?>(exceptions.get(0)) + } + + val items = result.getRelatedItems() + if (items.isEmpty()) { + return Single.error?>(NullPointerException("Got no search results.")) + } + try { + val results = items.stream() + .filter { item: InfoItem? -> item!!.getInfoType() == InfoType.STREAM || item.getInfoType() == InfoType.PLAYLIST || item.getInfoType() == InfoType.CHANNEL } + .map { item: InfoItem? -> + this.createInfoItemMediaItem( + item!! + ) + }.toList() + return Single.just?>(results) + } catch (e: Exception) { + return Single.error?>(e) + } + } + + private fun handleSearchError(throwable: Throwable?) { + Log.e(TAG, "Search error: " + throwable) + disposePrepareOrPlayCommands() + playbackError(R.string.content_not_supported, PlaybackStateCompat.ERROR_CODE_NOT_SUPPORTED) + } + + override fun onPrepareFromUri( + uri: Uri, + playWhenReady: Boolean, + extras: Bundle? + ) { + disposePrepareOrPlayCommands() + playbackError(R.string.content_not_supported, PlaybackStateCompat.ERROR_CODE_NOT_SUPPORTED) + } + + override fun onCommand( + player: Player, + command: String, + extras: Bundle?, + cb: ResultReceiver? + ): Boolean { + return false + } + + fun onSearch( + query: String, + result: MediaBrowserServiceCompat.Result?> + ) { + result.detach() + if (searchDisposable != null) { + searchDisposable!!.dispose() + } + searchDisposable = searchMusicBySongTitle(query) + .flatMap?>( + Function { result: SearchInfo? -> + this.mediaItemsFromInfoItemList( + result!! + ) + } + ) + .subscribeOn(Schedulers.io()) + .subscribe( + Consumer { result: MutableList? -> + result.sendResult( + result + ) + }, + Consumer { throwable: Throwable? -> this.handleSearchError(throwable) } + ) + } + + companion object { + private val TAG: String = MediaBrowserConnector::class.java.getSimpleName() + + private val ID_AUTHORITY = BuildConfig.APPLICATION_ID + private val ID_ROOT = "//" + ID_AUTHORITY + private const val ID_BOOKMARKS = "playlists" + private const val ID_HISTORY = "history" + private const val ID_INFO_ITEM = "item" + + private const val ID_LOCAL = "local" + private const val ID_REMOTE = "remote" + private const val ID_URL = "url" + private const val ID_STREAM = "stream" + private const val ID_PLAYLIST = "playlist" + private const val ID_CHANNEL = "channel" + + private fun infoItemTypeToString(type: InfoType): String { + return when (type) { + InfoType.STREAM -> ID_STREAM + InfoType.PLAYLIST -> ID_PLAYLIST + InfoType.CHANNEL -> ID_CHANNEL + else -> throw IllegalStateException("Unexpected value: " + type) + } + } + + private fun infoItemTypeFromString(type: String): InfoType { + return when (type) { + ID_STREAM -> InfoType.STREAM + ID_PLAYLIST -> InfoType.PLAYLIST + ID_CHANNEL -> InfoType.CHANNEL + else -> throw IllegalStateException("Unexpected value: " + type) + } + } + + private fun parseError(): ContentNotAvailableException { + return ContentNotAvailableException("Failed to parse media ID") + } + + @Throws(ContentNotAvailableException::class) + private fun extractPlayQueueFromInfoItemMediaId( + path: MutableList, + url: String? + ): Single? { + if (path.size != 2) { + throw parseError() + } + val infoItemType = infoItemTypeFromString(path.get(0)) + val serviceId = path.get(1).toInt() + return when (infoItemType) { + InfoType.STREAM -> ExtractorHelper.getStreamInfo(serviceId, url, false) + .map(Function { info: StreamInfo? -> SinglePlayQueue(info) }) + + InfoType.PLAYLIST -> ExtractorHelper.getPlaylistInfo(serviceId, url, false) + .map(Function { info: PlaylistInfo? -> PlaylistPlayQueue(info) }) + + InfoType.CHANNEL -> ExtractorHelper.getChannelInfo(serviceId, url, false) + .map( + Function { info: ChannelInfo? -> + val playableTab = info!!.getTabs() + .stream() + .filter { tab: ListLinkHandler? -> ChannelTabHelper.isStreamsTab(tab) } + .findFirst() + if (playableTab.isPresent()) { + return@map ChannelTabPlayQueue( + serviceId, + ListLinkHandler(playableTab.get()) + ) + } else { + throw ContentNotAvailableException("No streams tab found") + } + } + ) + + else -> throw parseError() + } + } + } +} From 0594b6596ad4c8cf18bd9305521155e223ca8ec0 Mon Sep 17 00:00:00 2001 From: Profpatsch Date: Fri, 7 Feb 2025 11:01:31 +0100 Subject: [PATCH 22/31] MediaBrowserConnector: convert to Kotlin (minimal compilation fixes) --- .../schabi/newpipe/player/PlayerService.kt | 4 +- .../mediabrowser/MediaBrowserConnector.kt | 248 +++++++++--------- 2 files changed, 125 insertions(+), 127 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayerService.kt b/app/src/main/java/org/schabi/newpipe/player/PlayerService.kt index cf1d2fca7c4..7ef0f008942 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PlayerService.kt +++ b/app/src/main/java/org/schabi/newpipe/player/PlayerService.kt @@ -203,7 +203,7 @@ class PlayerService : MediaBrowserServiceCompat() { override fun onLoadChildren( parentId: String, - result: Result?> + result: Result> ) { result.detach() val disposable = mediaBrowserConnector!!.onLoadChildren(parentId) @@ -220,7 +220,7 @@ class PlayerService : MediaBrowserServiceCompat() { override fun onSearch( query: String, extras: Bundle?, - result: Result?> + result: Result> ) { mediaBrowserConnector!!.onSearch(query, result) } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.kt b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.kt index bcccbda48a6..c205a4222a5 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.kt +++ b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.kt @@ -1,5 +1,6 @@ package org.schabi.newpipe.player.mediabrowser +import android.annotation.SuppressLint import android.content.ContentResolver import android.net.Uri import android.os.Bundle @@ -22,7 +23,6 @@ import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.core.SingleSource import io.reactivex.rxjava3.disposables.Disposable -import io.reactivex.rxjava3.functions.Consumer import io.reactivex.rxjava3.functions.Function import io.reactivex.rxjava3.schedulers.Schedulers import org.schabi.newpipe.BuildConfig @@ -56,6 +56,7 @@ import org.schabi.newpipe.local.playlist.RemotePlaylistManager import org.schabi.newpipe.player.PlayerService import org.schabi.newpipe.player.playqueue.ChannelTabPlayQueue import org.schabi.newpipe.player.playqueue.PlayQueue +import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue import org.schabi.newpipe.player.playqueue.SinglePlayQueue import org.schabi.newpipe.util.ChannelTabHelper import org.schabi.newpipe.util.ExtractorHelper @@ -97,7 +98,7 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { val builder = MediaDescriptionCompat.Builder() builder.setMediaId(mediaId) builder.setTitle(folderName) - val resources = playerService.getResources() + val resources = playerService.resources builder.setIconUri( Uri.Builder() .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) @@ -129,7 +130,7 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { val extras = Bundle() extras.putString( MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, - playerService.getResources().getString(R.string.tab_bookmarks) + playerService.resources.getString(R.string.tab_bookmarks) ) builder.setExtras(extras) return MediaBrowserCompat.MediaItem( @@ -141,17 +142,17 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { private fun createInfoItemMediaItem(item: InfoItem): MediaBrowserCompat.MediaItem { val builder = MediaDescriptionCompat.Builder() builder.setMediaId(createMediaIdForInfoItem(item)) - .setTitle(item.getName()) + .setTitle(item.name) - when (item.getInfoType()) { - InfoType.STREAM -> builder.setSubtitle((item as StreamInfoItem).getUploaderName()) - InfoType.PLAYLIST -> builder.setSubtitle((item as PlaylistInfoItem).getUploaderName()) - InfoType.CHANNEL -> builder.setSubtitle((item as ChannelInfoItem).getDescription()) + when (item.infoType) { + InfoType.STREAM -> builder.setSubtitle((item as StreamInfoItem).uploaderName) + InfoType.PLAYLIST -> builder.setSubtitle((item as PlaylistInfoItem).uploaderName) + InfoType.CHANNEL -> builder.setSubtitle((item as ChannelInfoItem).description) else -> {} } - val thumbnails = item.getThumbnails() + val thumbnails = item.thumbnails if (!thumbnails.isEmpty()) { - builder.setIconUri(Uri.parse(thumbnails.get(0)!!.getUrl())) + builder.setIconUri(Uri.parse(thumbnails.get(0)!!.url)) } return MediaBrowserCompat.MediaItem( builder.build(), @@ -177,9 +178,9 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { private fun buildInfoItemMediaId(item: InfoItem): Uri.Builder { return buildMediaId() .appendPath(ID_INFO_ITEM) - .appendPath(infoItemTypeToString(item.getInfoType())) - .appendPath(item.getServiceId().toString()) - .appendQueryParameter(ID_URL, item.getUrl()) + .appendPath(infoItemTypeToString(item.infoType)) + .appendPath(item.serviceId.toString()) + .appendQueryParameter(ID_URL, item.url) } private fun createMediaIdForInfoItem(remote: Boolean, playlistId: Long): String { @@ -211,11 +212,11 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { ): MediaBrowserCompat.MediaItem { val builder = MediaDescriptionCompat.Builder() builder.setMediaId(createMediaIdForPlaylistIndex(true, playlistId, index)) - .setTitle(item.getName()) - .setSubtitle(item.getUploaderName()) - val thumbnails = item.getThumbnails() + .setTitle(item.name) + .setSubtitle(item.uploaderName) + val thumbnails = item.thumbnails if (!thumbnails.isEmpty()) { - builder.setIconUri(Uri.parse(thumbnails.get(0)!!.getUrl())) + builder.setIconUri(Uri.parse(thumbnails.get(0)!!.url)) } return MediaBrowserCompat.MediaItem( @@ -260,22 +261,22 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { return MediaBrowserServiceCompat.BrowserRoot(ID_ROOT, extras) } - fun onLoadChildren(parentId: String): Single?>? { + fun onLoadChildren(parentId: String): Single> { if (MainActivity.DEBUG) { Log.d(TAG, String.format("MediaBrowserService.onLoadChildren(%s)", parentId)) } try { val parentIdUri = Uri.parse(parentId) - val path: MutableList = ArrayList(parentIdUri.getPathSegments()) + val path: MutableList = ArrayList(parentIdUri.pathSegments) if (path.isEmpty()) { - val mediaItems: MutableList = - ArrayList() + val mediaItems: MutableList = + ArrayList() mediaItems.add( createRootMediaItem( ID_BOOKMARKS, - playerService.getResources().getString( + playerService.resources.getString( R.string.tab_bookmarks_short ), R.drawable.ic_bookmark_white @@ -284,11 +285,11 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { mediaItems.add( createRootMediaItem( ID_HISTORY, - playerService.getResources().getString(R.string.action_history), + playerService.resources.getString(R.string.action_history), R.drawable.ic_history_white ) ) - return Single.just?>(mediaItems) + return Single.just(mediaItems) } val uriType = path.get(0) @@ -316,16 +317,16 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { else -> throw parseError() } } catch (e: ContentNotAvailableException) { - return Single.error?>(e) + return Single.error(e) } } - private fun populateHistory(): Single?>? { + private fun populateHistory(): Single> { val streamHistory = getDatabase().streamHistoryDAO() val history = streamHistory.getHistory().firstOrError() - return history.map?>( - Function { items: MutableList? -> - items!!.stream() + return history.map>( + Function { items -> + items.stream() .map { streamHistoryEntry: StreamHistoryEntry? -> this.createHistoryMediaItem( streamHistoryEntry!! @@ -360,7 +361,7 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { return database!! } - private fun getPlaylists(): Flowable?>? { + private fun getPlaylists(): Flowable> { if (localPlaylistManager == null) { localPlaylistManager = LocalPlaylistManager(getDatabase()) } @@ -381,14 +382,14 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { sessionConnector = MediaSessionConnector(mediaSession) sessionConnector.setMetadataDeduplicationEnabled(true) sessionConnector.setPlaybackPreparer(this) - playerService.setSessionToken(mediaSession.getSessionToken()) + playerService.setSessionToken(mediaSession.sessionToken) setupBookmarksNotifications() } private fun setupBookmarksNotifications() { - bookmarksNotificationsDisposable = getPlaylists()!!.subscribe( - Consumer { playlistMetadataEntries: MutableList? -> + bookmarksNotificationsDisposable = getPlaylists().subscribe( + { playlistMetadataEntries -> playerService.notifyChildrenChanged( ID_BOOKMARKS ) @@ -405,12 +406,12 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { // Suppress Sonar warning replace list collection by Stream.toList call, as this method is only // available in Android API 34 and not currently available with desugaring - private fun populateBookmarks(): Single?>? { - val playlists = getPlaylists()!!.firstOrError() - return playlists.map?>( - Function { playlist: MutableList? -> - playlist!!.stream() - .map { playlist: PlaylistLocalItem? -> + private fun populateBookmarks(): Single> { + val playlists = getPlaylists().firstOrError() + return playlists.map>( + { playlist: List -> + playlist.stream() + .map { playlist: PlaylistLocalItem? -> this.createPlaylistMediaItem( playlist!! ) @@ -420,14 +421,14 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { ) } - private fun populateLocalPlaylist(playlistId: Long): Single?>? { + private fun populateLocalPlaylist(playlistId: Long): Single> { val playlist = localPlaylistManager!!.getPlaylistStreams(playlistId).firstOrError() - return playlist.map?>( - Function { items: MutableList? -> - val results: MutableList = - ArrayList() + return playlist.map>( + { items: List -> + val results: MutableList = + ArrayList() var index = 0 - for (item in items!!) { + for (item in items) { results.add(createLocalPlaylistStreamMediaItem(playlistId, item, index)) ++index } @@ -436,20 +437,23 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { ) } - private fun getRemotePlaylist(playlistId: Long): Single?>?>? { + // Suppress Sonar warning replace list collection by Stream.toList call, as this method is only + // available in Android API 34 and not currently available with desugaring + @SuppressLint("NewApi") + private fun getRemotePlaylist(playlistId: Long): Single>> { val playlistFlow = remotePlaylistManager!!.getPlaylist(playlistId).firstOrError() - return playlistFlow.flatMap?>?>( - Function { item: MutableList? -> - val playlist = item!!.get(0) + return playlistFlow.flatMap>>( + { item: MutableList -> + val playlist = item.get(0) val playlistInfo = ExtractorHelper.getPlaylistInfo( - playlist.getServiceId(), - playlist.getUrl(), false + playlist.serviceId, + playlist.url, false ) - playlistInfo.flatMap?>?>( - Function { info: PlaylistInfo? -> - val infoItemsPage = info!!.getRelatedItems() - if (!info.getErrors().isEmpty()) { - val errors: MutableList = ArrayList(info.getErrors()) + playlistInfo.flatMap>>( + { info: PlaylistInfo -> + val infoItemsPage = info.relatedItems + if (!info.errors.isEmpty()) { + val errors: MutableList = ArrayList(info.errors) errors.removeIf { obj: Throwable? -> ContentNotSupportedException::class.java.isInstance( @@ -458,16 +462,16 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { } if (!errors.isEmpty()) { - return@flatMap Single.error?>?>( + return@flatMap Single.error( errors.get(0) ) } } - Single.just?>?>( + Single.just>>( IntStream.range(0, infoItemsPage.size) - .mapToObj?>( - java.util.function.IntFunction { i: kotlin.Int -> - android.util.Pair.create( + .mapToObj>( + java.util.function.IntFunction { i: Int -> + Pair.create( infoItemsPage.get(i), i ) @@ -481,17 +485,17 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { ) } - private fun populateRemotePlaylist(playlistId: Long): Single?>? { - return getRemotePlaylist(playlistId)!!.map?>( - Function { items: MutableList?>? -> - items!!.stream() - .map { pair: Pair? -> + private fun populateRemotePlaylist(playlistId: Long): Single> { + return getRemotePlaylist(playlistId).map>( + { items -> + items + .map { pair -> createRemotePlaylistStreamMediaItem( playlistId, - pair!!.first!!, - pair.second!! + pair.first, + pair.second ) - }.toList() + } } ) } @@ -505,11 +509,11 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { playbackError(errorInfo.messageStringId, PlaybackStateCompat.ERROR_CODE_APP_ERROR) } - private fun extractLocalPlayQueue(playlistId: Long, index: Int): Single? { + private fun extractLocalPlayQueue(playlistId: Long, index: Int): Single { return localPlaylistManager!!.getPlaylistStreams(playlistId) .firstOrError() - .map( - Function { items: MutableList? -> + .map( + { items: MutableList? -> val infoItems = items!!.stream() .map { obj: PlaylistStreamEntry? -> obj!!.toStreamInfoItem() } .collect(Collectors.toList()) @@ -518,21 +522,20 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { ) } - private fun extractRemotePlayQueue(playlistId: Long, index: Int): Single? { - return getRemotePlaylist(playlistId)!!.map( - Function { items: MutableList?>? -> - val infoItems = items!!.stream() - .map { pair: Pair? -> pair!!.first } - .toList() + private fun extractRemotePlayQueue(playlistId: Long, index: Int): Single { + return getRemotePlaylist(playlistId).map( + { items -> + val infoItems = items + .map { pair -> pair.first } SinglePlayQueue(infoItems, index) } ) } - private fun extractPlayQueueFromMediaId(mediaId: String?): Single? { + private fun extractPlayQueueFromMediaId(mediaId: String?): Single { try { val mediaIdUri = Uri.parse(mediaId) - val path: MutableList = ArrayList(mediaIdUri.getPathSegments()) + val path: MutableList = ArrayList(mediaIdUri.pathSegments) if (path.isEmpty()) { throw parseError() @@ -556,7 +559,7 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { else -> throw parseError() } } catch (e: ContentNotAvailableException) { - return Single.error(e) + return Single.error(e) } } @@ -564,7 +567,7 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { private fun extractPlayQueueFromPlaylistMediaId( path: MutableList, url: String? - ): Single? { + ): Single { if (path.isEmpty()) { throw parseError() } @@ -592,7 +595,7 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { val serviceId = path.get(0).toInt() return ExtractorHelper.getPlaylistInfo(serviceId, url, false) - .map(Function { info: PlaylistInfo? -> PlaylistPlayQueue(info) }) + .map({ info: PlaylistInfo? -> PlaylistPlayQueue(info) }) } else -> throw parseError() @@ -601,8 +604,8 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { @Throws(ContentNotAvailableException::class) private fun extractPlayQueueFromHistoryMediaId( - path: MutableList - ): Single? { + path: List + ): Single { if (path.size != 1) { throw parseError() } @@ -610,7 +613,7 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { val streamId = path.get(0).toLong() return getDatabase().streamHistoryDAO().getHistory() .firstOrError() - .map( + .map( Function { items: MutableList? -> val infoItems = items!!.stream() .filter { it: StreamHistoryEntry? -> it!!.streamId == streamId } @@ -657,14 +660,14 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( - Consumer { playQueue: PlayQueue? -> + { playQueue: PlayQueue? -> sessionConnector.setCustomErrorMessage(null) NavigationHelper.playOnBackgroundPlayer( playerService, playQueue, playWhenReady ) }, - Consumer { throwable: Throwable? -> + { throwable: Throwable -> playbackError( ErrorInfo( throwable, UserAction.PLAY_STREAM, @@ -687,7 +690,7 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { ) } - private fun searchMusicBySongTitle(query: String?): Single { + private fun searchMusicBySongTitle(query: String?): Single { val serviceId = ServiceHelper.getSelectedServiceId(playerService) return ExtractorHelper.searchFor( serviceId, query, @@ -695,36 +698,36 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { ) } - private fun mediaItemsFromInfoItemList(result: ListInfo): SingleSource?> { - val exceptions = result.getErrors() + private fun mediaItemsFromInfoItemList(result: ListInfo): SingleSource> { + val exceptions = result.errors if (!exceptions.isEmpty() && !( exceptions.size == 1 && exceptions.get(0) is NothingFoundException ) ) { - return Single.error?>(exceptions.get(0)) + return Single.error(exceptions.get(0)) } val items = result.getRelatedItems() if (items.isEmpty()) { - return Single.error?>(NullPointerException("Got no search results.")) + return Single.error(NullPointerException("Got no search results.")) } try { - val results = items.stream() - .filter { item: InfoItem? -> item!!.getInfoType() == InfoType.STREAM || item.getInfoType() == InfoType.PLAYLIST || item.getInfoType() == InfoType.CHANNEL } - .map { item: InfoItem? -> + val results = items + .filter { item: InfoItem -> item.infoType == InfoType.STREAM || item.infoType == InfoType.PLAYLIST || item.infoType == InfoType.CHANNEL } + .map { item: InfoItem -> this.createInfoItemMediaItem( - item!! + item ) - }.toList() - return Single.just?>(results) + } + return Single.just(results) } catch (e: Exception) { - return Single.error?>(e) + return Single.error(e) } } - private fun handleSearchError(throwable: Throwable?) { + private fun handleSearchError(throwable: Throwable) { Log.e(TAG, "Search error: " + throwable) disposePrepareOrPlayCommands() playbackError(R.string.content_not_supported, PlaybackStateCompat.ERROR_CODE_NOT_SUPPORTED) @@ -750,28 +753,23 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { fun onSearch( query: String, - result: MediaBrowserServiceCompat.Result?> + result: MediaBrowserServiceCompat.Result> ) { result.detach() if (searchDisposable != null) { searchDisposable!!.dispose() } searchDisposable = searchMusicBySongTitle(query) - .flatMap?>( - Function { result: SearchInfo? -> - this.mediaItemsFromInfoItemList( - result!! - ) - } - ) + .flatMap> + { + this.mediaItemsFromInfoItemList( + it + ) + } .subscribeOn(Schedulers.io()) .subscribe( - Consumer { result: MutableList? -> - result.sendResult( - result - ) - }, - Consumer { throwable: Throwable? -> this.handleSearchError(throwable) } + { result.sendResult(it) }, + { throwable: Throwable -> this.handleSearchError(throwable) } ) } @@ -815,9 +813,9 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { @Throws(ContentNotAvailableException::class) private fun extractPlayQueueFromInfoItemMediaId( - path: MutableList, + path: List, url: String? - ): Single? { + ): Single { if (path.size != 2) { throw parseError() } @@ -825,19 +823,19 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { val serviceId = path.get(1).toInt() return when (infoItemType) { InfoType.STREAM -> ExtractorHelper.getStreamInfo(serviceId, url, false) - .map(Function { info: StreamInfo? -> SinglePlayQueue(info) }) + .map(Function { info: StreamInfo? -> SinglePlayQueue(info) }) InfoType.PLAYLIST -> ExtractorHelper.getPlaylistInfo(serviceId, url, false) - .map(Function { info: PlaylistInfo? -> PlaylistPlayQueue(info) }) + .map(Function { info: PlaylistInfo? -> PlaylistPlayQueue(info) }) - InfoType.CHANNEL -> ExtractorHelper.getChannelInfo(serviceId, url, false) - .map( - Function { info: ChannelInfo? -> - val playableTab = info!!.getTabs() + InfoType.CHANNEL -> { + ExtractorHelper.getChannelInfo(serviceId, url, false) + .map { info: ChannelInfo -> + val playableTab = info.tabs .stream() .filter { tab: ListLinkHandler? -> ChannelTabHelper.isStreamsTab(tab) } .findFirst() - if (playableTab.isPresent()) { + if (playableTab.isPresent) { return@map ChannelTabPlayQueue( serviceId, ListLinkHandler(playableTab.get()) @@ -846,7 +844,7 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { throw ContentNotAvailableException("No streams tab found") } } - ) + } else -> throw parseError() } From 8584f03f7a9e9b8371d9bdc57b3a281ae90b817b Mon Sep 17 00:00:00 2001 From: Haggai Eran Date: Tue, 17 Sep 2024 22:37:45 +0300 Subject: [PATCH 23/31] MediaBrowserConnector: simplify index, remove unstable warning --- .../schabi/newpipe/player/PlayerService.kt | 2 -- .../mediabrowser/MediaBrowserConnector.kt | 30 +++++-------------- 2 files changed, 8 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayerService.kt b/app/src/main/java/org/schabi/newpipe/player/PlayerService.kt index 7ef0f008942..92faa610451 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PlayerService.kt +++ b/app/src/main/java/org/schabi/newpipe/player/PlayerService.kt @@ -78,8 +78,6 @@ class PlayerService : MediaBrowserServiceCompat() { } } - // Suppress Sonar warning to not always return the same value, as we need to do some actions - // before returning override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { if (DEBUG) { Log.d( diff --git a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.kt b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.kt index c205a4222a5..a49fd635945 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.kt +++ b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.kt @@ -1,6 +1,5 @@ package org.schabi.newpipe.player.mediabrowser -import android.annotation.SuppressLint import android.content.ContentResolver import android.net.Uri import android.os.Bundle @@ -67,7 +66,6 @@ import java.lang.IllegalStateException import java.lang.NullPointerException import java.util.ArrayList import java.util.stream.Collectors -import java.util.stream.IntStream class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { private val playerService: PlayerService @@ -404,8 +402,6 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { } } - // Suppress Sonar warning replace list collection by Stream.toList call, as this method is only - // available in Android API 34 and not currently available with desugaring private fun populateBookmarks(): Single> { val playlists = getPlaylists().firstOrError() return playlists.map>( @@ -437,19 +433,16 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { ) } - // Suppress Sonar warning replace list collection by Stream.toList call, as this method is only - // available in Android API 34 and not currently available with desugaring - @SuppressLint("NewApi") - private fun getRemotePlaylist(playlistId: Long): Single>> { + private fun getRemotePlaylist(playlistId: Long): Single>> { val playlistFlow = remotePlaylistManager!!.getPlaylist(playlistId).firstOrError() - return playlistFlow.flatMap>>( - { item: MutableList -> + return playlistFlow.flatMap>>( + { item: List -> val playlist = item.get(0) val playlistInfo = ExtractorHelper.getPlaylistInfo( playlist.serviceId, playlist.url, false ) - playlistInfo.flatMap>>( + playlistInfo.flatMap>>( { info: PlaylistInfo -> val infoItemsPage = info.relatedItems if (!info.errors.isEmpty()) { @@ -467,17 +460,10 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { ) } } - Single.just>>( - IntStream.range(0, infoItemsPage.size) - .mapToObj>( - java.util.function.IntFunction { i: Int -> - Pair.create( - infoItemsPage.get(i), - i - ) - } - ) - .toList()!! + Single.just>>( + infoItemsPage.withIndex().map { + Pair(it.value, it.index) + } ) } ) From ae2003a7b38c902b878f4417d0b27e4068fbbb3a Mon Sep 17 00:00:00 2001 From: Haggai Eran Date: Tue, 17 Sep 2024 22:40:59 +0300 Subject: [PATCH 24/31] media browser: rename remote -> isRemote --- .../player/mediabrowser/MediaBrowserConnector.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.kt b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.kt index a49fd635945..78affa012ce 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.kt +++ b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.kt @@ -120,8 +120,8 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { private fun createPlaylistMediaItem(playlist: PlaylistLocalItem): MediaBrowserCompat.MediaItem { val builder = MediaDescriptionCompat.Builder() - val remote = playlist is PlaylistRemoteEntity - builder.setMediaId(createMediaIdForInfoItem(remote, playlist.uid)) + val isRemote = playlist is PlaylistRemoteEntity + builder.setMediaId(createMediaIdForInfoItem(isRemote, playlist.uid)) .setTitle(playlist.orderingName) .setIconUri(Uri.parse(playlist.thumbnailUrl)) @@ -168,8 +168,8 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { .appendPath(playlistType) } - private fun buildLocalPlaylistItemMediaId(remote: Boolean, playlistId: Long): Uri.Builder { - return buildPlaylistMediaId(if (remote) ID_REMOTE else ID_LOCAL) + private fun buildLocalPlaylistItemMediaId(isRemote: Boolean, playlistId: Long): Uri.Builder { + return buildPlaylistMediaId(if (isRemote) ID_REMOTE else ID_LOCAL) .appendPath(playlistId.toString()) } @@ -181,8 +181,8 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { .appendQueryParameter(ID_URL, item.url) } - private fun createMediaIdForInfoItem(remote: Boolean, playlistId: Long): String { - return buildLocalPlaylistItemMediaId(remote, playlistId) + private fun createMediaIdForInfoItem(isRemote: Boolean, playlistId: Long): String { + return buildLocalPlaylistItemMediaId(isRemote, playlistId) .build().toString() } @@ -224,11 +224,11 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { } private fun createMediaIdForPlaylistIndex( - remote: Boolean, + isRemote: Boolean, playlistId: Long, index: Int ): String { - return buildLocalPlaylistItemMediaId(remote, playlistId) + return buildLocalPlaylistItemMediaId(isRemote, playlistId) .appendPath(index.toString()) .build().toString() } From 718ff0fbfe9445f6b397d38565ae394fe8dd5381 Mon Sep 17 00:00:00 2001 From: Haggai Eran Date: Tue, 17 Sep 2024 22:46:55 +0300 Subject: [PATCH 25/31] media browser: pass media ID to parsing error exceptions --- .../mediabrowser/MediaBrowserConnector.kt | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.kt b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.kt index 78affa012ce..1291ad678c5 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.kt +++ b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.kt @@ -308,11 +308,11 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { } } Log.w(TAG, "Unknown playlist URI: " + parentId) - throw parseError() + throw parseError(parentId) } ID_HISTORY -> return populateHistory() - else -> throw parseError() + else -> throw parseError(parentId) } } catch (e: ContentNotAvailableException) { return Single.error(e) @@ -518,13 +518,13 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { ) } - private fun extractPlayQueueFromMediaId(mediaId: String?): Single { + private fun extractPlayQueueFromMediaId(mediaId: String): Single { try { val mediaIdUri = Uri.parse(mediaId) val path: MutableList = ArrayList(mediaIdUri.pathSegments) if (path.isEmpty()) { - throw parseError() + throw parseError(mediaId) } val uriType: String? = path.get(0) @@ -532,17 +532,19 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { return when (uriType) { ID_BOOKMARKS -> extractPlayQueueFromPlaylistMediaId( + mediaId, path, mediaIdUri.getQueryParameter(ID_URL) ) - ID_HISTORY -> extractPlayQueueFromHistoryMediaId(path) + ID_HISTORY -> extractPlayQueueFromHistoryMediaId(mediaId, path) ID_INFO_ITEM -> extractPlayQueueFromInfoItemMediaId( + mediaId, path, mediaIdUri.getQueryParameter(ID_URL) ) - else -> throw parseError() + else -> throw parseError(mediaId) } } catch (e: ContentNotAvailableException) { return Single.error(e) @@ -551,11 +553,12 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { @Throws(ContentNotAvailableException::class) private fun extractPlayQueueFromPlaylistMediaId( + mediaId: String, path: MutableList, url: String? ): Single { if (path.isEmpty()) { - throw parseError() + throw parseError(mediaId) } val playlistType = path.get(0) @@ -564,7 +567,7 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { when (playlistType) { ID_LOCAL, ID_REMOTE -> { if (path.size != 2) { - throw parseError() + throw parseError(mediaId) } val playlistId = path.get(0).toLong() val index = path.get(1).toInt() @@ -576,7 +579,7 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { ID_URL -> { if (path.size != 1) { - throw parseError() + throw parseError(mediaId) } val serviceId = path.get(0).toInt() @@ -584,16 +587,17 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { .map({ info: PlaylistInfo? -> PlaylistPlayQueue(info) }) } - else -> throw parseError() + else -> throw parseError(mediaId) } } @Throws(ContentNotAvailableException::class) private fun extractPlayQueueFromHistoryMediaId( + mediaId: String, path: List ): Single { if (path.size != 1) { - throw parseError() + throw parseError(mediaId) } val streamId = path.get(0).toLong() @@ -793,17 +797,18 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { } } - private fun parseError(): ContentNotAvailableException { - return ContentNotAvailableException("Failed to parse media ID") + private fun parseError(mediaId: String): ContentNotAvailableException { + return ContentNotAvailableException("Failed to parse media ID $mediaId") } @Throws(ContentNotAvailableException::class) private fun extractPlayQueueFromInfoItemMediaId( + mediaId: String, path: List, url: String? ): Single { if (path.size != 2) { - throw parseError() + throw parseError(mediaId) } val infoItemType = infoItemTypeFromString(path.get(0)) val serviceId = path.get(1).toInt() @@ -832,7 +837,7 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { } } - else -> throw parseError() + else -> throw parseError(mediaId) } } } From f974bf123e6b0050a45239d396d3241869a5e52c Mon Sep 17 00:00:00 2001 From: Siddhesh Naik Date: Sat, 12 Oct 2024 21:04:45 +0530 Subject: [PATCH 26/31] Addressed review comments --- .../fragments/detail/VideoDetailFragment.java | 3 ++- .../newpipe/local/dialog/PlaylistDialog.java | 2 +- .../newpipe/player/PlayQueueActivity.java | 27 ++++++++++++------- .../schabi/newpipe/player/PlayerService.kt | 4 +-- .../PlayerServiceExtendedEventListener.java | 4 ++- .../newpipe/player/helper/PlayerHolder.java | 2 +- .../mediabrowser/MediaBrowserConnector.kt | 5 +++- 7 files changed, 30 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 40a22103b0b..87edd9f2963 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -229,6 +229,7 @@ public final class VideoDetailFragment private ContentObserver settingsContentObserver; @Nullable private PlayerService playerService; + @Nullable private Player player; private final PlayerHolder playerHolder = PlayerHolder.getInstance(); @@ -236,7 +237,7 @@ public final class VideoDetailFragment // Service management //////////////////////////////////////////////////////////////////////////*/ @Override - public void onServiceConnected(final Player connectedPlayer, + public void onServiceConnected(@Nullable final Player connectedPlayer, final PlayerService connectedPlayerService, final boolean playAfterConnect) { player = connectedPlayer; diff --git a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistDialog.java b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistDialog.java index 612c3818187..da408bb50aa 100644 --- a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistDialog.java +++ b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistDialog.java @@ -163,7 +163,7 @@ public static Disposable createCorrespondingDialog( * @return the disposable that was created */ public static Disposable showForPlayQueue( - final Player player, + @NonNull final Player player, @NonNull final FragmentManager fragmentManager) { final List streamEntities = Stream.of(player.getPlayQueue()) diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java b/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java index 195baecbda8..dc959afea01 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java @@ -61,6 +61,7 @@ public final class PlayQueueActivity extends AppCompatActivity private static final int MENU_ID_AUDIO_TRACK = 71; + @Nullable private Player player; private boolean serviceBound; @@ -137,30 +138,38 @@ public boolean onOptionsItemSelected(final MenuItem item) { NavigationHelper.openSettings(this); return true; case R.id.action_append_playlist: - PlaylistDialog.showForPlayQueue(player, getSupportFragmentManager()); + if (player != null) { + PlaylistDialog.showForPlayQueue(player, getSupportFragmentManager()); + } return true; case R.id.action_playback_speed: openPlaybackParameterDialog(); return true; case R.id.action_mute: - player.toggleMute(); + if (player != null) { + player.toggleMute(); + } return true; case R.id.action_system_audio: startActivity(new Intent(Settings.ACTION_SOUND_SETTINGS)); return true; case R.id.action_switch_main: - this.player.setRecovery(); - NavigationHelper.playOnMainPlayer(this, player.getPlayQueue(), true); + if (player != null) { + this.player.setRecovery(); + NavigationHelper.playOnMainPlayer(this, player.getPlayQueue(), true); + } return true; case R.id.action_switch_popup: - if (PermissionHelper.isPopupEnabledElseAsk(this)) { + if (PermissionHelper.isPopupEnabledElseAsk(this) && player != null) { this.player.setRecovery(); NavigationHelper.playOnPopupPlayer(this, player.getPlayQueue(), true); } return true; case R.id.action_switch_background: - this.player.setRecovery(); - NavigationHelper.playOnBackgroundPlayer(this, player.getPlayQueue(), true); + if (player != null) { + this.player.setRecovery(); + NavigationHelper.playOnBackgroundPlayer(this, player.getPlayQueue(), true); + } return true; } @@ -309,7 +318,7 @@ public void onMove(final int sourceIndex, final int targetIndex) { @Override public void onSwiped(final int index) { - if (index != -1) { + if (index != -1 && player != null) { player.getPlayQueue().remove(index); } } @@ -659,7 +668,7 @@ private void buildAudioTrackMenu() { * @param itemId index of the selected item */ private void onAudioTrackClick(final int itemId) { - if (player.getCurrentMetadata() == null) { + if (player == null || player.getCurrentMetadata() == null) { return; } player.getCurrentMetadata().getMaybeAudioTrack().ifPresent(audioTrack -> { diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayerService.kt b/app/src/main/java/org/schabi/newpipe/player/PlayerService.kt index 92faa610451..5c6b76f1a23 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PlayerService.kt +++ b/app/src/main/java/org/schabi/newpipe/player/PlayerService.kt @@ -234,9 +234,7 @@ class PlayerService : MediaBrowserServiceCompat() { return playerService.get() } - fun getPlayer(): Player? { - return playerService.get()!!.player - } + fun getPlayer(): Player? = playerService.get()?.player } companion object { diff --git a/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceExtendedEventListener.java b/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceExtendedEventListener.java index 8effe2f0e93..15852088b6f 100644 --- a/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceExtendedEventListener.java +++ b/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceExtendedEventListener.java @@ -1,10 +1,12 @@ package org.schabi.newpipe.player.event; +import androidx.annotation.Nullable; + import org.schabi.newpipe.player.PlayerService; import org.schabi.newpipe.player.Player; public interface PlayerServiceExtendedEventListener extends PlayerServiceEventListener { - void onServiceConnected(Player player, + void onServiceConnected(@Nullable Player player, PlayerService playerService, boolean playAfterConnect); void onServiceDisconnected(); diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java index b55a6547ab7..ba4fb377260 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java @@ -166,7 +166,7 @@ public void onServiceConnected(final ComponentName compName, final IBinder servi } final PlayerService.LocalBinder localBinder = (PlayerService.LocalBinder) service; - playerService = localBinder.getService(); + playerService = localBinder.getPlayer().getService(); player = localBinder.getPlayer(); if (listener != null) { listener.onServiceConnected(player, playerService, playAfterConnect); diff --git a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.kt b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.kt index 1291ad678c5..4ff536d23d9 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.kt +++ b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.kt @@ -720,7 +720,10 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { private fun handleSearchError(throwable: Throwable) { Log.e(TAG, "Search error: " + throwable) disposePrepareOrPlayCommands() - playbackError(R.string.content_not_supported, PlaybackStateCompat.ERROR_CODE_NOT_SUPPORTED) + sessionConnector.setCustomErrorMessage( + playerService.getString(R.string.search_no_results), + PlaybackStateCompat.ERROR_CODE_APP_ERROR, + ) } override fun onPrepareFromUri( From c0d229a040107ca3a7674337b15eaad970ff7100 Mon Sep 17 00:00:00 2001 From: Siddhesh Naik Date: Sat, 7 Dec 2024 02:08:06 +0530 Subject: [PATCH 27/31] Addressed review comments --- .../main/java/org/schabi/newpipe/player/Player.java | 12 +++++++----- .../player/mediabrowser/MediaBrowserConnector.kt | 10 +++++----- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index 95692cbed6c..b19df82fa01 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -45,7 +45,6 @@ import static org.schabi.newpipe.util.ListHelper.getPopupResolutionIndex; import static org.schabi.newpipe.util.ListHelper.getResolutionIndex; import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; -import static java.util.Objects.requireNonNull; import static java.util.concurrent.TimeUnit.MILLISECONDS; import android.content.BroadcastReceiver; @@ -87,8 +86,8 @@ import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.ErrorUtil; import org.schabi.newpipe.error.UserAction; -import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.Image; +import org.schabi.newpipe.extractor.stream.AudioStream; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.extractor.stream.VideoStream; @@ -119,9 +118,9 @@ import org.schabi.newpipe.util.DependentPreferenceHelper; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.NavigationHelper; -import org.schabi.newpipe.util.image.PicassoHelper; import org.schabi.newpipe.util.SerializedCache; import org.schabi.newpipe.util.StreamTypeUtil; +import org.schabi.newpipe.util.image.PicassoHelper; import java.util.List; import java.util.Optional; @@ -416,9 +415,12 @@ public void handleIntent(@NonNull final Intent intent) { == com.google.android.exoplayer2.Player.STATE_IDLE) { simpleExoPlayer.prepare(); } + // Seeks to a specific index and position in the player if the queue index has changed. if (playQueue.getIndex() != newQueue.getIndex()) { - simpleExoPlayer.seekTo(newQueue.getIndex(), - requireNonNull(newQueue.getItem()).getRecoveryPosition()); + final PlayQueueItem queueItem = newQueue.getItem(); + if (queueItem != null) { + simpleExoPlayer.seekTo(newQueue.getIndex(), queueItem.getRecoveryPosition()); + } } simpleExoPlayer.setPlayWhenReady(playWhenReady); diff --git a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.kt b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.kt index 4ff536d23d9..cba5ba51c4c 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.kt +++ b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.kt @@ -120,20 +120,20 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { private fun createPlaylistMediaItem(playlist: PlaylistLocalItem): MediaBrowserCompat.MediaItem { val builder = MediaDescriptionCompat.Builder() - val isRemote = playlist is PlaylistRemoteEntity - builder.setMediaId(createMediaIdForInfoItem(isRemote, playlist.uid)) + builder + .setMediaId(createMediaIdForInfoItem(playlist is PlaylistRemoteEntity, playlist.uid)) .setTitle(playlist.orderingName) - .setIconUri(Uri.parse(playlist.thumbnailUrl)) + .setIconUri(playlist.thumbnailUrl?.let { Uri.parse(it) }) val extras = Bundle() extras.putString( MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, - playerService.resources.getString(R.string.tab_bookmarks) + playerService.resources.getString(R.string.tab_bookmarks), ) builder.setExtras(extras) return MediaBrowserCompat.MediaItem( builder.build(), - MediaBrowserCompat.MediaItem.FLAG_BROWSABLE + MediaBrowserCompat.MediaItem.FLAG_BROWSABLE, ) } From 92e7feb6bfe02198e03bfd8d25b44b48083b267c Mon Sep 17 00:00:00 2001 From: Haggai Eran Date: Sat, 14 Dec 2024 17:31:51 +0200 Subject: [PATCH 28/31] PlayerService: return appropriate IBinder depending on the action --- app/src/main/java/org/schabi/newpipe/player/PlayerService.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayerService.kt b/app/src/main/java/org/schabi/newpipe/player/PlayerService.kt index 5c6b76f1a23..a57b84bf582 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PlayerService.kt +++ b/app/src/main/java/org/schabi/newpipe/player/PlayerService.kt @@ -180,7 +180,8 @@ class PlayerService : MediaBrowserServiceCompat() { } override fun onBind(intent: Intent): IBinder? { - if (SERVICE_INTERFACE == intent.getAction()) { + if (SERVICE_INTERFACE == intent.action) { + // For actions related to the media browser service, pass the onBind to the superclass return super.onBind(intent) } return mBinder From 85b00f584906cb833cdf9fc5262f7fc2756445b9 Mon Sep 17 00:00:00 2001 From: Profpatsch Date: Fri, 7 Feb 2025 19:06:04 +0100 Subject: [PATCH 29/31] MediaBrowserConnector: separate context from playerService These are all Context operations, not specific to our playerService. --- .../mediabrowser/MediaBrowserConnector.kt | 39 +++++++++---------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.kt b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.kt index cba5ba51c4c..954ccc45d98 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.kt +++ b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.kt @@ -1,6 +1,7 @@ package org.schabi.newpipe.player.mediabrowser import android.content.ContentResolver +import android.content.Context import android.net.Uri import android.os.Bundle import android.os.ResultReceiver @@ -67,8 +68,8 @@ import java.lang.NullPointerException import java.util.ArrayList import java.util.stream.Collectors -class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { - private val playerService: PlayerService +class MediaBrowserConnector(private val playerService: PlayerService) : PlaybackPreparer { + private val context: Context private val sessionConnector: MediaSessionConnector private val mediaSession: MediaSessionCompat @@ -96,7 +97,7 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { val builder = MediaDescriptionCompat.Builder() builder.setMediaId(mediaId) builder.setTitle(folderName) - val resources = playerService.resources + val resources = context.resources builder.setIconUri( Uri.Builder() .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) @@ -109,7 +110,7 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { val extras = Bundle() extras.putString( MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, - playerService.getString(R.string.app_name) + context.getString(R.string.app_name) ) builder.setExtras(extras) return MediaBrowserCompat.MediaItem( @@ -128,7 +129,7 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { val extras = Bundle() extras.putString( MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, - playerService.resources.getString(R.string.tab_bookmarks), + context.resources.getString(R.string.tab_bookmarks), ) builder.setExtras(extras) return MediaBrowserCompat.MediaItem( @@ -274,7 +275,7 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { mediaItems.add( createRootMediaItem( ID_BOOKMARKS, - playerService.resources.getString( + context.resources.getString( R.string.tab_bookmarks_short ), R.drawable.ic_bookmark_white @@ -283,7 +284,7 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { mediaItems.add( createRootMediaItem( ID_HISTORY, - playerService.resources.getString(R.string.action_history), + context.resources.getString(R.string.action_history), R.drawable.ic_history_white ) ) @@ -354,7 +355,7 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { private fun getDatabase(): AppDatabase { if (database == null) { - database = NewPipeDatabase.getInstance(playerService) + database = NewPipeDatabase.getInstance(context) } return database!! } @@ -375,20 +376,16 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { var bookmarksNotificationsDisposable: Disposable? = null init { - this.playerService = playerService + this.context = playerService mediaSession = MediaSessionCompat(playerService, TAG) sessionConnector = MediaSessionConnector(mediaSession) sessionConnector.setMetadataDeduplicationEnabled(true) sessionConnector.setPlaybackPreparer(this) playerService.setSessionToken(mediaSession.sessionToken) - setupBookmarksNotifications() - } - - private fun setupBookmarksNotifications() { bookmarksNotificationsDisposable = getPlaylists().subscribe( { playlistMetadataEntries -> - playerService.notifyChildrenChanged( + this.context.notifyChildrenChanged( ID_BOOKMARKS ) } @@ -488,7 +485,7 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { private fun playbackError(@StringRes resId: Int, code: Int) { playerService.stopForImmediateReusing() - sessionConnector.setCustomErrorMessage(playerService.getString(resId), code) + sessionConnector.setCustomErrorMessage(context.getString(resId), code) } private fun playbackError(errorInfo: ErrorInfo) { @@ -653,7 +650,7 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { { playQueue: PlayQueue? -> sessionConnector.setCustomErrorMessage(null) NavigationHelper.playOnBackgroundPlayer( - playerService, playQueue, + context, playQueue, playWhenReady ) }, @@ -681,7 +678,7 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { } private fun searchMusicBySongTitle(query: String?): Single { - val serviceId = ServiceHelper.getSelectedServiceId(playerService) + val serviceId = ServiceHelper.getSelectedServiceId(context) return ExtractorHelper.searchFor( serviceId, query, ArrayList(), "" @@ -692,9 +689,9 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { val exceptions = result.errors if (!exceptions.isEmpty() && !( - exceptions.size == 1 && - exceptions.get(0) is NothingFoundException - ) + exceptions.size == 1 && + exceptions.get(0) is NothingFoundException + ) ) { return Single.error(exceptions.get(0)) } @@ -721,7 +718,7 @@ class MediaBrowserConnector(playerService: PlayerService) : PlaybackPreparer { Log.e(TAG, "Search error: " + throwable) disposePrepareOrPlayCommands() sessionConnector.setCustomErrorMessage( - playerService.getString(R.string.search_no_results), + context.getString(R.string.search_no_results), PlaybackStateCompat.ERROR_CODE_APP_ERROR, ) } From 10458b92f8a9f914d852bc12d83079482d1212a0 Mon Sep 17 00:00:00 2001 From: Profpatsch Date: Fri, 7 Feb 2025 19:10:17 +0100 Subject: [PATCH 30/31] MediaBrowserConnector: move init to top & simplify --- .../mediabrowser/MediaBrowserConnector.kt | 61 ++++++++----------- 1 file changed, 25 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.kt b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.kt index 954ccc45d98..adba2801590 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.kt +++ b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.kt @@ -69,9 +69,9 @@ import java.util.ArrayList import java.util.stream.Collectors class MediaBrowserConnector(private val playerService: PlayerService) : PlaybackPreparer { - private val context: Context + private val context: Context = playerService private val sessionConnector: MediaSessionConnector - private val mediaSession: MediaSessionCompat + private val mediaSession: MediaSessionCompat = MediaSessionCompat(playerService, TAG) private var database: AppDatabase? = null private var localPlaylistManager: LocalPlaylistManager? = null @@ -79,13 +79,31 @@ class MediaBrowserConnector(private val playerService: PlayerService) : Playback private var prepareOrPlayDisposable: Disposable? = null private var searchDisposable: Disposable? = null + var bookmarksNotificationsDisposable: Disposable + + init { + sessionConnector = MediaSessionConnector(mediaSession) + sessionConnector.setMetadataDeduplicationEnabled(true) + sessionConnector.setPlaybackPreparer(this) + playerService.setSessionToken(mediaSession.sessionToken) + + bookmarksNotificationsDisposable = getPlaylists().subscribe( + { playlistMetadataEntries -> + playerService.notifyChildrenChanged( + ID_BOOKMARKS + ) + } + ) + } + + fun getSessionConnector(): MediaSessionConnector { return sessionConnector } fun release() { disposePrepareOrPlayCommands() - disposeBookmarksNotifications() + bookmarksNotificationsDisposable.dispose() mediaSession.release() } @@ -373,32 +391,6 @@ class MediaBrowserConnector(private val playerService: PlayerService) : Playback ) } - var bookmarksNotificationsDisposable: Disposable? = null - - init { - this.context = playerService - mediaSession = MediaSessionCompat(playerService, TAG) - sessionConnector = MediaSessionConnector(mediaSession) - sessionConnector.setMetadataDeduplicationEnabled(true) - sessionConnector.setPlaybackPreparer(this) - playerService.setSessionToken(mediaSession.sessionToken) - - bookmarksNotificationsDisposable = getPlaylists().subscribe( - { playlistMetadataEntries -> - this.context.notifyChildrenChanged( - ID_BOOKMARKS - ) - } - ) - } - - private fun disposeBookmarksNotifications() { - if (bookmarksNotificationsDisposable != null) { - bookmarksNotificationsDisposable!!.dispose() - bookmarksNotificationsDisposable = null - } - } - private fun populateBookmarks(): Single> { val playlists = getPlaylists().firstOrError() return playlists.map>( @@ -616,10 +608,7 @@ class MediaBrowserConnector(private val playerService: PlayerService) : Playback } private fun disposePrepareOrPlayCommands() { - if (prepareOrPlayDisposable != null) { - prepareOrPlayDisposable!!.dispose() - prepareOrPlayDisposable = null - } + prepareOrPlayDisposable?.dispose() } override fun onPrepare(playWhenReady: Boolean) { @@ -689,9 +678,9 @@ class MediaBrowserConnector(private val playerService: PlayerService) : Playback val exceptions = result.errors if (!exceptions.isEmpty() && !( - exceptions.size == 1 && - exceptions.get(0) is NothingFoundException - ) + exceptions.size == 1 && + exceptions.get(0) is NothingFoundException + ) ) { return Single.error(exceptions.get(0)) } From df29bea0e6ed02f557766899bbe41dcb5c25ab56 Mon Sep 17 00:00:00 2001 From: Profpatsch Date: Fri, 7 Feb 2025 19:44:52 +0100 Subject: [PATCH 31/31] MediaBrowserConnector: simplify database methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We can assume fetching the database singleton is fast. Same goes for the MergedPlaylistManager. So let’s put them into a getter. --- .../mediabrowser/MediaBrowserConnector.kt | 47 ++++++------------- 1 file changed, 14 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.kt b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.kt index adba2801590..3e40834b48c 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.kt +++ b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.kt @@ -19,7 +19,6 @@ import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector.PlaybackPreparer import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers -import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.core.SingleSource import io.reactivex.rxjava3.disposables.Disposable @@ -73,9 +72,13 @@ class MediaBrowserConnector(private val playerService: PlayerService) : Playback private val sessionConnector: MediaSessionConnector private val mediaSession: MediaSessionCompat = MediaSessionCompat(playerService, TAG) - private var database: AppDatabase? = null - private var localPlaylistManager: LocalPlaylistManager? = null - private var remotePlaylistManager: RemotePlaylistManager? = null + private val database: AppDatabase + get() = NewPipeDatabase.getInstance(context) + private val mergedPlaylists + get() = MergedPlaylistManager.getMergedOrderedPlaylists( + LocalPlaylistManager(database), + RemotePlaylistManager(database) + ) private var prepareOrPlayDisposable: Disposable? = null private var searchDisposable: Disposable? = null @@ -87,7 +90,7 @@ class MediaBrowserConnector(private val playerService: PlayerService) : Playback sessionConnector.setPlaybackPreparer(this) playerService.setSessionToken(mediaSession.sessionToken) - bookmarksNotificationsDisposable = getPlaylists().subscribe( + bookmarksNotificationsDisposable = mergedPlaylists.subscribe( { playlistMetadataEntries -> playerService.notifyChildrenChanged( ID_BOOKMARKS @@ -96,7 +99,6 @@ class MediaBrowserConnector(private val playerService: PlayerService) : Playback ) } - fun getSessionConnector(): MediaSessionConnector { return sessionConnector } @@ -339,8 +341,7 @@ class MediaBrowserConnector(private val playerService: PlayerService) : Playback } private fun populateHistory(): Single> { - val streamHistory = getDatabase().streamHistoryDAO() - val history = streamHistory.getHistory().firstOrError() + val history = database.streamHistoryDAO().getHistory().firstOrError() return history.map>( Function { items -> items.stream() @@ -371,28 +372,8 @@ class MediaBrowserConnector(private val playerService: PlayerService) : Playback ) } - private fun getDatabase(): AppDatabase { - if (database == null) { - database = NewPipeDatabase.getInstance(context) - } - return database!! - } - - private fun getPlaylists(): Flowable> { - if (localPlaylistManager == null) { - localPlaylistManager = LocalPlaylistManager(getDatabase()) - } - if (remotePlaylistManager == null) { - remotePlaylistManager = RemotePlaylistManager(getDatabase()) - } - return MergedPlaylistManager.getMergedOrderedPlaylists( - localPlaylistManager, - remotePlaylistManager - ) - } - private fun populateBookmarks(): Single> { - val playlists = getPlaylists().firstOrError() + val playlists = mergedPlaylists.firstOrError() return playlists.map>( { playlist: List -> playlist.stream() @@ -407,7 +388,7 @@ class MediaBrowserConnector(private val playerService: PlayerService) : Playback } private fun populateLocalPlaylist(playlistId: Long): Single> { - val playlist = localPlaylistManager!!.getPlaylistStreams(playlistId).firstOrError() + val playlist = LocalPlaylistManager(database).getPlaylistStreams(playlistId).firstOrError() return playlist.map>( { items: List -> val results: MutableList = @@ -423,7 +404,7 @@ class MediaBrowserConnector(private val playerService: PlayerService) : Playback } private fun getRemotePlaylist(playlistId: Long): Single>> { - val playlistFlow = remotePlaylistManager!!.getPlaylist(playlistId).firstOrError() + val playlistFlow = RemotePlaylistManager(database).getPlaylist(playlistId).firstOrError() return playlistFlow.flatMap>>( { item: List -> val playlist = item.get(0) @@ -485,7 +466,7 @@ class MediaBrowserConnector(private val playerService: PlayerService) : Playback } private fun extractLocalPlayQueue(playlistId: Long, index: Int): Single { - return localPlaylistManager!!.getPlaylistStreams(playlistId) + return LocalPlaylistManager(database).getPlaylistStreams(playlistId) .firstOrError() .map( { items: MutableList? -> @@ -590,7 +571,7 @@ class MediaBrowserConnector(private val playerService: PlayerService) : Playback } val streamId = path.get(0).toLong() - return getDatabase().streamHistoryDAO().getHistory() + return database.streamHistoryDAO().getHistory() .firstOrError() .map( Function { items: MutableList? ->