Skip to content

Commit 52932e5

Browse files
committed
Media browser interface to show playlists on Android Auto
1 parent ebbaf0d commit 52932e5

1 file changed

Lines changed: 194 additions & 6 deletions

File tree

Lines changed: 194 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,54 @@
11
package org.schabi.newpipe.player.mediabrowser;
22

3+
import android.net.Uri;
34
import android.os.Bundle;
4-
import android.support.v4.media.MediaBrowserCompat;
5+
import android.os.ResultReceiver;
56
import android.support.v4.media.MediaBrowserCompat.MediaItem;
7+
import android.support.v4.media.MediaDescriptionCompat;
68
import android.support.v4.media.session.MediaSessionCompat;
9+
import android.support.v4.media.session.PlaybackStateCompat;
710
import android.util.Log;
811

912
import androidx.annotation.NonNull;
1013
import androidx.annotation.Nullable;
14+
import androidx.annotation.StringRes;
1115
import androidx.media.MediaBrowserServiceCompat;
1216
import androidx.media.MediaBrowserServiceCompat.Result;
17+
import androidx.media.utils.MediaConstants;
1318

19+
import com.google.android.exoplayer2.Player;
1420
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
1521

22+
import org.schabi.newpipe.NewPipeDatabase;
23+
import org.schabi.newpipe.R;
24+
import org.schabi.newpipe.database.AppDatabase;
25+
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
26+
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
27+
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
1628
import org.schabi.newpipe.player.PlayerService;
29+
import org.schabi.newpipe.player.playqueue.PlayQueue;
30+
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
31+
import org.schabi.newpipe.util.NavigationHelper;
1732

1833
import java.util.ArrayList;
1934
import java.util.List;
35+
import java.util.stream.Collectors;
2036

21-
public class MediaBrowserConnector {
37+
public class MediaBrowserConnector implements MediaSessionConnector.PlaybackPreparer {
2238
private static final String TAG = MediaBrowserConnector.class.getSimpleName();
2339

2440
private final PlayerService playerService;
2541
private final @NonNull MediaSessionConnector sessionConnector;
2642
private final @NonNull MediaSessionCompat mediaSession;
2743

44+
private AppDatabase database;
45+
private LocalPlaylistManager localPlaylistManager;
2846
public MediaBrowserConnector(@NonNull final PlayerService playerService) {
2947
this.playerService = playerService;
3048
mediaSession = new MediaSessionCompat(playerService, TAG);
3149
sessionConnector = new MediaSessionConnector(mediaSession);
3250
sessionConnector.setMetadataDeduplicationEnabled(true);
51+
sessionConnector.setPlaybackPreparer(this);
3352
playerService.setSessionToken(mediaSession.getSessionToken());
3453
}
3554

@@ -42,7 +61,53 @@ public void release() {
4261
}
4362

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

47112
@Nullable
48113
public MediaBrowserServiceCompat.BrowserRoot onGetRoot(@NonNull final String clientPackageName,
@@ -51,15 +116,138 @@ public MediaBrowserServiceCompat.BrowserRoot onGetRoot(@NonNull final String cli
51116
Log.d(TAG, String.format("MediaBrowserService.onGetRoot(%s, %s, %s)",
52117
clientPackageName, clientUid, rootHints));
53118

54-
return new MediaBrowserServiceCompat.BrowserRoot(MY_MEDIA_ROOT_ID, null);
119+
return new MediaBrowserServiceCompat.BrowserRoot(ID_ROOT, null);
55120
}
56121

57122
public void onLoadChildren(@NonNull final String parentId,
58123
@NonNull final Result<List<MediaItem>> result) {
59124
Log.d(TAG, String.format("MediaBrowserService.onLoadChildren(%s)", parentId));
60125

61-
final List<MediaBrowserCompat.MediaItem> mediaItems = new ArrayList<>();
62-
126+
final List<MediaItem> mediaItems = new ArrayList<>();
127+
final var parentIdUri = Uri.parse(parentId);
128+
129+
if (parentId.equals(ID_ROOT)) {
130+
mediaItems.add(
131+
createRootMediaItem(ID_BOOKMARKS,
132+
playerService.getResources().getString(R.string.tab_bookmarks)));
133+
134+
} else if (parentId.startsWith(ID_BOOKMARKS)) {
135+
final var path = parentIdUri.getPathSegments();
136+
if (path.size() == 2) {
137+
populateBookmarks(mediaItems);
138+
} else if (path.size() == 3) {
139+
final var playlistId = Long.valueOf(path.get(2));
140+
populatePlaylist(playlistId, mediaItems);
141+
} else {
142+
Log.w(TAG, "Unknown playlist uri " + parentId);
143+
}
144+
}
63145
result.sendResult(mediaItems);
64146
}
147+
148+
private LocalPlaylistManager getPlaylistManager() {
149+
if (database == null) {
150+
database = NewPipeDatabase.getInstance(playerService);
151+
}
152+
if (localPlaylistManager == null) {
153+
localPlaylistManager = new LocalPlaylistManager(database);
154+
}
155+
return localPlaylistManager;
156+
}
157+
158+
private void populateBookmarks(final List<MediaItem> mediaItems) {
159+
// TODO async
160+
final var playlists = getPlaylistManager().getPlaylists().blockingFirst();
161+
mediaItems.addAll(playlists.stream().map(this::createPlaylistMediaItem)
162+
.collect(Collectors.toList()));
163+
}
164+
165+
private void populatePlaylist(final Long playlistId, final List<MediaItem> mediaItems) {
166+
// TODO async
167+
getPlaylistManager().getPlaylists().blockingFirst()
168+
.stream().filter(it -> it.uid == playlistId).findFirst()
169+
.ifPresent(playlist -> {
170+
final var items = getPlaylistManager().getPlaylistStreams(playlist.uid)
171+
.blockingFirst();
172+
int index = 0;
173+
for (final var item : items) {
174+
mediaItems.add(createPlaylistStreamMediaItem(playlist, item, index));
175+
++index;
176+
}
177+
});
178+
}
179+
180+
private void playbackError(@StringRes final int resId, final int code) {
181+
sessionConnector.setCustomErrorMessage(playerService.getString(resId), code);
182+
}
183+
184+
private PlayQueue extractPlayQueueFromMediaId(final String mediaId) {
185+
final Uri mediaIdUri = Uri.parse(mediaId);
186+
if (mediaIdUri == null) {
187+
// TODO handle errors
188+
return null;
189+
}
190+
if (mediaId.startsWith(ID_BOOKMARKS)) {
191+
final var path = mediaIdUri.getPathSegments();
192+
if (path.size() == 4) {
193+
final long playlistId = Long.parseLong(path.get(2));
194+
final int index = Integer.parseInt(path.get(3));
195+
196+
final var items = getPlaylistManager().getPlaylists().blockingFirst()
197+
.stream().filter(it -> it.uid == playlistId).findFirst()
198+
.map(playlist -> getPlaylistManager().getPlaylistStreams(playlist.uid)
199+
.blockingFirst().stream()
200+
.map(PlaylistStreamEntry::toStreamInfoItem)
201+
.collect(Collectors.toList())).orElse(new ArrayList<>());
202+
203+
return new SinglePlayQueue(items, index);
204+
}
205+
}
206+
207+
playbackError(R.string.error_http_not_found,
208+
PlaybackStateCompat.ERROR_CODE_NOT_SUPPORTED);
209+
return null;
210+
}
211+
212+
@Override
213+
public long getSupportedPrepareActions() {
214+
return PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID;
215+
}
216+
217+
@Override
218+
public void onPrepare(final boolean playWhenReady) {
219+
// No need to prepare
220+
}
221+
222+
@Override
223+
public void onPrepareFromMediaId(@NonNull final String mediaId, final boolean playWhenReady,
224+
@Nullable final Bundle extras) {
225+
Log.d(TAG, String.format("MediaBrowserConnector.onPrepareFromMediaId(%s, %s, %s)",
226+
mediaId, playWhenReady, extras));
227+
228+
if (playWhenReady) {
229+
final var playQueue = extractPlayQueueFromMediaId(mediaId);
230+
if (playQueue != null) {
231+
NavigationHelper.playOnBackgroundPlayer(playerService, playQueue, true);
232+
}
233+
}
234+
}
235+
236+
@Override
237+
public void onPrepareFromSearch(@NonNull final String query, final boolean playWhenReady,
238+
@Nullable final Bundle extras) {
239+
playbackError(R.string.content_not_supported, PlaybackStateCompat.ERROR_CODE_NOT_SUPPORTED);
240+
}
241+
242+
@Override
243+
public void onPrepareFromUri(@NonNull final Uri uri, final boolean playWhenReady,
244+
@Nullable final Bundle extras) {
245+
playbackError(R.string.content_not_supported, PlaybackStateCompat.ERROR_CODE_NOT_SUPPORTED);
246+
}
247+
248+
@Override
249+
public boolean onCommand(@NonNull final Player player, @NonNull final String command,
250+
@Nullable final Bundle extras, @Nullable final ResultReceiver cb) {
251+
return false;
252+
}
65253
}

0 commit comments

Comments
 (0)