Merge f33d72fc2b
into 6d694518fe
This commit is contained in:
commit
8c0ce48993
|
@ -64,6 +64,9 @@
|
|||
<intent-filter>
|
||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.media.browse.MediaBrowserService"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<activity
|
||||
|
@ -423,5 +426,10 @@
|
|||
<meta-data
|
||||
android:name="com.samsung.android.multidisplay.keep_process_alive"
|
||||
android:value="true" />
|
||||
<!-- Android Auto -->
|
||||
<meta-data android:name="com.google.android.gms.car.application"
|
||||
android:resource="@xml/automotive_app_desc" />
|
||||
<meta-data android:name="com.google.android.gms.car.notification.SmallIcon"
|
||||
android:resource="@mipmap/ic_launcher" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
|
|
@ -3,6 +3,7 @@ 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 java.time.OffsetDateTime
|
||||
|
||||
data class StreamHistoryEntry(
|
||||
|
@ -27,4 +28,19 @@ 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.thumbnailUrl = streamEntity.thumbnailUrl
|
||||
|
||||
return item
|
||||
}
|
||||
}
|
||||
|
|
|
@ -302,7 +302,7 @@ public final class Player implements PlaybackListener, Listener {
|
|||
// 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)
|
||||
);
|
||||
}
|
||||
|
@ -415,6 +415,10 @@ public final class Player implements PlaybackListener, Listener {
|
|||
== 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)
|
||||
|
|
|
@ -21,46 +21,75 @@ package org.schabi.newpipe.player;
|
|||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
import android.app.Service;
|
||||
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.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 Service {
|
||||
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);
|
||||
|
||||
player = new Player(this);
|
||||
mediaBrowserConnector = new MediaBrowserConnector(this);
|
||||
}
|
||||
|
||||
private void initializePlayerIfNeeded() {
|
||||
if (player == null) {
|
||||
player = new Player(this);
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
@ -69,13 +98,14 @@ public final class PlayerService extends Service {
|
|||
}
|
||||
|
||||
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
|
||||
&& 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
|
||||
&& (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
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
player.handleIntent(intent);
|
||||
initializePlayerIfNeeded();
|
||||
Objects.requireNonNull(player).handleIntent(intent);
|
||||
player.UIs().get(MediaSessionPlayerUi.class)
|
||||
.ifPresent(ui -> ui.handleMediaButtonIntent(intent));
|
||||
|
||||
|
@ -87,7 +117,7 @@ public final class PlayerService extends Service {
|
|||
Log.d(TAG, "stopForImmediateReusing() called");
|
||||
}
|
||||
|
||||
if (!player.exoPlayerIsNull()) {
|
||||
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
|
||||
|
@ -98,7 +128,7 @@ public final class PlayerService extends Service {
|
|||
@Override
|
||||
public void onTaskRemoved(final Intent rootIntent) {
|
||||
super.onTaskRemoved(rootIntent);
|
||||
if (!player.videoPlayerSelected()) {
|
||||
if (player != null && !player.videoPlayerSelected()) {
|
||||
return;
|
||||
}
|
||||
onDestroy();
|
||||
|
@ -111,7 +141,15 @@ public final class PlayerService extends Service {
|
|||
if (DEBUG) {
|
||||
Log.d(TAG, "destroy() called");
|
||||
}
|
||||
|
||||
cleanup();
|
||||
|
||||
if (mediaBrowserConnector != null) {
|
||||
mediaBrowserConnector.release();
|
||||
mediaBrowserConnector = null;
|
||||
}
|
||||
|
||||
compositeDisposableLoadChildren.clear();
|
||||
}
|
||||
|
||||
private void cleanup() {
|
||||
|
@ -132,21 +170,49 @@ public final class PlayerService extends Service {
|
|||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
public static class LocalBinder extends Binder {
|
||||
@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<List<MediaItem>> result) {
|
||||
result.detach();
|
||||
final Disposable disposable = mediaBrowserConnector.onLoadChildren(parentId)
|
||||
.subscribe(result::sendResult);
|
||||
compositeDisposableLoadChildren.add(disposable);
|
||||
}
|
||||
|
||||
public static final class LocalBinder extends Binder {
|
||||
private final WeakReference<PlayerService> 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;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,364 @@
|
|||
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 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.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;
|
||||
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 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 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());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public MediaSessionConnector getSessionConnector() {
|
||||
return sessionConnector;
|
||||
}
|
||||
|
||||
public void release() {
|
||||
disposePrepareOrPlayCommands();
|
||||
mediaSession.release();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
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";
|
||||
|
||||
@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 PlaylistMetadataEntry playlist) {
|
||||
final var builder = new MediaDescriptionCompat.Builder();
|
||||
builder.setMediaId(createMediaIdForPlaylist(playlist.uid))
|
||||
.setTitle(playlist.name)
|
||||
.setIconUri(Uri.parse(playlist.thumbnailUrl));
|
||||
|
||||
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,
|
||||
@NonNull final PlaylistStreamEntry item,
|
||||
final int index) {
|
||||
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);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private String createMediaIdForPlaylistIndex(final long playlistId, final int index) {
|
||||
return createMediaIdForPlaylist(playlistId) + '/' + index;
|
||||
}
|
||||
|
||||
@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(ID_ROOT, null);
|
||||
}
|
||||
|
||||
public Single<List<MediaItem>> onLoadChildren(@NonNull final String parentId) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, String.format("MediaBrowserService.onLoadChildren(%s)", parentId));
|
||||
}
|
||||
|
||||
final List<MediaItem> mediaItems = new ArrayList<>();
|
||||
|
||||
if (parentId.equals(ID_ROOT)) {
|
||||
mediaItems.add(
|
||||
createRootMediaItem(ID_BOOKMARKS,
|
||||
playerService.getResources().getString(R.string.tab_bookmarks),
|
||||
R.drawable.ic_bookmark));
|
||||
mediaItems.add(
|
||||
createRootMediaItem(ID_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<String> 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 (parentId.equals(ID_HISTORY)) {
|
||||
return populateHistory();
|
||||
}
|
||||
return Single.just(mediaItems);
|
||||
}
|
||||
|
||||
private Single<List<MediaItem>> 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();
|
||||
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);
|
||||
}
|
||||
|
||||
private AppDatabase getDatabase() {
|
||||
if (database == null) {
|
||||
database = NewPipeDatabase.getInstance(playerService);
|
||||
}
|
||||
return database;
|
||||
}
|
||||
|
||||
private LocalPlaylistManager getPlaylistManager() {
|
||||
if (localPlaylistManager == null) {
|
||||
localPlaylistManager = new LocalPlaylistManager(getDatabase());
|
||||
}
|
||||
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<List<MediaItem>> populateBookmarks() {
|
||||
final var playlists = getPlaylistManager().getPlaylists().firstOrError();
|
||||
return playlists.map(playlist -> playlist.stream()
|
||||
.map(this::createPlaylistMediaItem)
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
private Single<List<MediaItem>> populatePlaylist(final long playlistId) {
|
||||
final var playlist = getPlaylistManager().getPlaylistStreams(playlistId).firstOrError();
|
||||
return playlist.map(items -> {
|
||||
final List<MediaItem> results = new ArrayList<>();
|
||||
int index = 0;
|
||||
for (final PlaylistStreamEntry 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 void playbackError(@NonNull final ErrorInfo errorInfo) {
|
||||
playbackError(errorInfo.getMessageStringId(), PlaybackStateCompat.ERROR_CODE_APP_ERROR);
|
||||
}
|
||||
|
||||
private Single<PlayQueue> extractPlayQueueFromMediaId(final String mediaId) {
|
||||
final Uri mediaIdUri = Uri.parse(mediaId);
|
||||
if (mediaIdUri == null) {
|
||||
return Single.error(new ContentNotAvailableException("Media ID cannot be parsed"));
|
||||
}
|
||||
|
||||
final List<String> 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<StreamInfoItem> 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<StreamInfoItem> 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"));
|
||||
}
|
||||
|
||||
@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(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);
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
|
@ -28,14 +28,19 @@ public class MediaSessionPlayerUi extends PlayerUi
|
|||
implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private static final String TAG = "MediaSessUi";
|
||||
|
||||
private MediaSessionCompat mediaSession;
|
||||
private MediaSessionConnector sessionConnector;
|
||||
@NonNull
|
||||
private final MediaSessionCompat mediaSession;
|
||||
@NonNull
|
||||
private final MediaSessionConnector sessionConnector;
|
||||
|
||||
private final String ignoreHardwareMediaButtonsKey;
|
||||
private boolean shouldIgnoreHardwareMediaButtons = false;
|
||||
|
||||
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);
|
||||
}
|
||||
|
@ -45,10 +50,8 @@ public class MediaSessionPlayerUi extends PlayerUi
|
|||
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());
|
||||
|
||||
|
@ -61,7 +64,6 @@ public class MediaSessionPlayerUi extends PlayerUi
|
|||
updateShouldIgnoreHardwareMediaButtons(player.getPrefs());
|
||||
player.getPrefs().registerOnSharedPreferenceChangeListener(this);
|
||||
|
||||
sessionConnector.setMetadataDeduplicationEnabled(true);
|
||||
sessionConnector.setMediaMetadataProvider(exoPlayer -> buildMediaMetadata());
|
||||
}
|
||||
|
||||
|
@ -69,26 +71,20 @@ public class MediaSessionPlayerUi extends PlayerUi
|
|||
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);
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
|
||||
|
||||
|
@ -111,7 +107,7 @@ public class MediaSessionPlayerUi extends PlayerUi
|
|||
}
|
||||
|
||||
public Optional<MediaSessionCompat.Token> getSessionToken() {
|
||||
return Optional.ofNullable(mediaSession).map(MediaSessionCompat::getSessionToken);
|
||||
return Optional.of(mediaSession.getSessionToken());
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
<automotiveApp>
|
||||
<uses name="media" />
|
||||
</automotiveApp>
|
Loading…
Reference in New Issue