Skip to content

Commit ab6cec9

Browse files
committed
Media browser interface to show playlists on Android Auto
1 parent 5cc94e5 commit ab6cec9

1 file changed

Lines changed: 212 additions & 5 deletions

File tree

app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserConnector.java

Lines changed: 212 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,37 +2,60 @@
22

33
import static org.schabi.newpipe.MainActivity.DEBUG;
44

5+
import android.net.Uri;
56
import android.os.Bundle;
6-
import android.support.v4.media.MediaBrowserCompat;
7+
import android.os.ResultReceiver;
78
import android.support.v4.media.MediaBrowserCompat.MediaItem;
9+
import android.support.v4.media.MediaDescriptionCompat;
810
import android.support.v4.media.session.MediaSessionCompat;
11+
import android.support.v4.media.session.PlaybackStateCompat;
912
import android.util.Log;
1013

1114
import androidx.annotation.NonNull;
1215
import androidx.annotation.Nullable;
16+
import androidx.annotation.StringRes;
1317
import androidx.media.MediaBrowserServiceCompat;
18+
import androidx.media.utils.MediaConstants;
1419

20+
import com.google.android.exoplayer2.Player;
1521
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
1622

23+
import org.schabi.newpipe.NewPipeDatabase;
24+
import org.schabi.newpipe.R;
25+
import org.schabi.newpipe.database.AppDatabase;
26+
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
27+
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
28+
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
1729
import org.schabi.newpipe.player.PlayerService;
30+
import org.schabi.newpipe.player.playqueue.PlayQueue;
31+
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
32+
import org.schabi.newpipe.util.NavigationHelper;
1833

1934
import java.util.ArrayList;
2035
import java.util.List;
36+
import java.util.stream.Collectors;
2137

38+
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
2239
import io.reactivex.rxjava3.core.Single;
40+
import io.reactivex.rxjava3.disposables.Disposable;
2341

24-
public class MediaBrowserConnector {
42+
public class MediaBrowserConnector implements MediaSessionConnector.PlaybackPreparer {
2543
private static final String TAG = MediaBrowserConnector.class.getSimpleName();
2644

2745
private final PlayerService playerService;
2846
private final @NonNull MediaSessionConnector sessionConnector;
2947
private final @NonNull MediaSessionCompat mediaSession;
3048

49+
private AppDatabase database;
50+
private LocalPlaylistManager localPlaylistManager;
51+
private Disposable prepareOrPlayDisposable;
52+
3153
public MediaBrowserConnector(@NonNull final PlayerService playerService) {
3254
this.playerService = playerService;
3355
mediaSession = new MediaSessionCompat(playerService, TAG);
3456
sessionConnector = new MediaSessionConnector(mediaSession);
3557
sessionConnector.setMetadataDeduplicationEnabled(true);
58+
sessionConnector.setPlaybackPreparer(this);
3659
playerService.setSessionToken(mediaSession.getSessionToken());
3760
}
3861

@@ -41,11 +64,58 @@ public MediaBrowserConnector(@NonNull final PlayerService playerService) {
4164
}
4265

4366
public void release() {
67+
disposePrepareOrPlayCommands();
4468
mediaSession.release();
4569
}
4670

4771
@NonNull
48-
private static final String MY_MEDIA_ROOT_ID = "media_root_id";
72+
private static final String ID_ROOT = "//${BuildConfig.APPLICATION_ID}/r";
73+
@NonNull
74+
private static final String ID_BOOKMARKS = ID_ROOT + "/playlists";
75+
76+
private MediaItem createRootMediaItem(final String mediaId, final String folderName) {
77+
final var builder = new MediaDescriptionCompat.Builder();
78+
builder.setMediaId(mediaId);
79+
builder.setTitle(folderName);
80+
81+
final var extras = new Bundle();
82+
extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE,
83+
playerService.getString(R.string.app_name));
84+
builder.setExtras(extras);
85+
return new MediaItem(builder.build(), MediaItem.FLAG_BROWSABLE);
86+
}
87+
88+
private MediaItem createPlaylistMediaItem(final PlaylistMetadataEntry playlist) {
89+
final var builder = new MediaDescriptionCompat.Builder();
90+
builder.setMediaId(createMediaIdForPlaylist(playlist.uid))
91+
.setTitle(playlist.name)
92+
.setIconUri(Uri.parse(playlist.thumbnailUrl));
93+
94+
final var extras = new Bundle();
95+
extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE,
96+
playerService.getResources().getString(R.string.tab_bookmarks));
97+
builder.setExtras(extras);
98+
return new MediaItem(builder.build(), MediaItem.FLAG_BROWSABLE);
99+
}
100+
101+
private String createMediaIdForPlaylist(final long playlistId) {
102+
return ID_BOOKMARKS + '/' + playlistId;
103+
}
104+
105+
private MediaItem createPlaylistStreamMediaItem(final long playlistId,
106+
final PlaylistStreamEntry item,
107+
final int index) {
108+
final var builder = new MediaDescriptionCompat.Builder();
109+
builder.setMediaId(createMediaIdForPlaylistIndex(playlistId, index))
110+
.setTitle(item.getStreamEntity().getTitle())
111+
.setIconUri(Uri.parse(item.getStreamEntity().getThumbnailUrl()));
112+
113+
return new MediaItem(builder.build(), MediaItem.FLAG_PLAYABLE);
114+
}
115+
116+
private String createMediaIdForPlaylistIndex(final long playlistId, final int index) {
117+
return createMediaIdForPlaylist(playlistId) + '/' + index;
118+
}
49119

50120
@Nullable
51121
public MediaBrowserServiceCompat.BrowserRoot onGetRoot(@NonNull final String clientPackageName,
@@ -56,16 +126,153 @@ public MediaBrowserServiceCompat.BrowserRoot onGetRoot(@NonNull final String cli
56126
clientPackageName, clientUid, rootHints));
57127
}
58128

59-
return new MediaBrowserServiceCompat.BrowserRoot(MY_MEDIA_ROOT_ID, null);
129+
return new MediaBrowserServiceCompat.BrowserRoot(ID_ROOT, null);
60130
}
61131

62132
public Single<List<MediaItem>> onLoadChildren(@NonNull final String parentId) {
63133
if (DEBUG) {
64134
Log.d(TAG, String.format("MediaBrowserService.onLoadChildren(%s)", parentId));
65135
}
66136

67-
final List<MediaBrowserCompat.MediaItem> mediaItems = new ArrayList<>();
137+
final List<MediaItem> mediaItems = new ArrayList<>();
138+
final var parentIdUri = Uri.parse(parentId);
68139

140+
if (parentId.equals(ID_ROOT)) {
141+
mediaItems.add(
142+
createRootMediaItem(ID_BOOKMARKS,
143+
playerService.getResources().getString(R.string.tab_bookmarks)));
144+
145+
} else if (parentId.startsWith(ID_BOOKMARKS)) {
146+
final var path = parentIdUri.getPathSegments();
147+
if (path.size() == 2) {
148+
return populateBookmarks();
149+
} else if (path.size() == 3) {
150+
final var playlistId = Long.parseLong(path.get(2));
151+
return populatePlaylist(playlistId);
152+
} else {
153+
Log.w(TAG, "Unknown playlist uri " + parentId);
154+
}
155+
}
69156
return Single.just(mediaItems);
70157
}
158+
159+
private LocalPlaylistManager getPlaylistManager() {
160+
if (database == null) {
161+
database = NewPipeDatabase.getInstance(playerService);
162+
}
163+
if (localPlaylistManager == null) {
164+
localPlaylistManager = new LocalPlaylistManager(database);
165+
}
166+
return localPlaylistManager;
167+
}
168+
169+
private Single<List<MediaItem>> populateBookmarks() {
170+
final var playlists = getPlaylistManager().getPlaylists().firstOrError();
171+
return playlists.map(playlist ->
172+
playlist.stream().map(this::createPlaylistMediaItem).collect(Collectors.toList()));
173+
}
174+
175+
private Single<List<MediaItem>> populatePlaylist(final long playlistId) {
176+
final var playlist = getPlaylistManager().getPlaylistStreams(playlistId).firstOrError();
177+
return playlist.map(items -> {
178+
final List<MediaItem> results = new ArrayList<>();
179+
int index = 0;
180+
for (final var item : items) {
181+
results.add(createPlaylistStreamMediaItem(playlistId, item, index));
182+
++index;
183+
}
184+
return results;
185+
});
186+
}
187+
188+
private void playbackError(@StringRes final int resId, final int code) {
189+
playerService.stopForImmediateReusing();
190+
sessionConnector.setCustomErrorMessage(playerService.getString(resId), code);
191+
}
192+
193+
private Single<PlayQueue> extractPlayQueueFromMediaId(final String mediaId) {
194+
final Uri mediaIdUri = Uri.parse(mediaId);
195+
if (mediaIdUri == null) {
196+
return Single.error(new NullPointerException());
197+
}
198+
if (mediaId.startsWith(ID_BOOKMARKS)) {
199+
final var path = mediaIdUri.getPathSegments();
200+
if (path.size() == 4) {
201+
final long playlistId = Long.parseLong(path.get(2));
202+
final int index = Integer.parseInt(path.get(3));
203+
204+
return getPlaylistManager()
205+
.getPlaylistStreams(playlistId)
206+
.firstOrError()
207+
.map(items -> {
208+
final var infoItems = items.stream()
209+
.map(PlaylistStreamEntry::toStreamInfoItem)
210+
.collect(Collectors.toList());
211+
return new SinglePlayQueue(infoItems, index);
212+
});
213+
}
214+
}
215+
216+
return Single.error(new NullPointerException());
217+
}
218+
219+
@Override
220+
public long getSupportedPrepareActions() {
221+
return PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID;
222+
}
223+
224+
private void disposePrepareOrPlayCommands() {
225+
if (prepareOrPlayDisposable != null) {
226+
prepareOrPlayDisposable.dispose();
227+
prepareOrPlayDisposable = null;
228+
}
229+
}
230+
231+
@Override
232+
public void onPrepare(final boolean playWhenReady) {
233+
disposePrepareOrPlayCommands();
234+
// No need to prepare
235+
}
236+
237+
@Override
238+
public void onPrepareFromMediaId(@NonNull final String mediaId, final boolean playWhenReady,
239+
@Nullable final Bundle extras) {
240+
if (DEBUG) {
241+
Log.d(TAG, String.format("MediaBrowserConnector.onPrepareFromMediaId(%s, %s, %s)",
242+
mediaId, playWhenReady, extras));
243+
}
244+
245+
disposePrepareOrPlayCommands();
246+
prepareOrPlayDisposable = extractPlayQueueFromMediaId(mediaId)
247+
.observeOn(AndroidSchedulers.mainThread())
248+
.subscribe(
249+
playQueue -> {
250+
sessionConnector.setCustomErrorMessage(null);
251+
NavigationHelper.playOnBackgroundPlayer(playerService, playQueue,
252+
playWhenReady);
253+
},
254+
throwable -> playbackError(R.string.error_http_not_found,
255+
PlaybackStateCompat.ERROR_CODE_NOT_SUPPORTED)
256+
);
257+
}
258+
259+
@Override
260+
public void onPrepareFromSearch(@NonNull final String query, final boolean playWhenReady,
261+
@Nullable final Bundle extras) {
262+
disposePrepareOrPlayCommands();
263+
playbackError(R.string.content_not_supported, PlaybackStateCompat.ERROR_CODE_NOT_SUPPORTED);
264+
}
265+
266+
@Override
267+
public void onPrepareFromUri(@NonNull final Uri uri, final boolean playWhenReady,
268+
@Nullable final Bundle extras) {
269+
disposePrepareOrPlayCommands();
270+
playbackError(R.string.content_not_supported, PlaybackStateCompat.ERROR_CODE_NOT_SUPPORTED);
271+
}
272+
273+
@Override
274+
public boolean onCommand(@NonNull final Player player, @NonNull final String command,
275+
@Nullable final Bundle extras, @Nullable final ResultReceiver cb) {
276+
return false;
277+
}
71278
}

0 commit comments

Comments
 (0)