This commit is contained in:
Haggai Eran 2023-10-30 11:38:37 +01:00 committed by GitHub
commit 8c0ce48993
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 491 additions and 34 deletions

View File

@ -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>

View File

@ -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
}
}

View File

@ -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)

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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());
}

View File

@ -0,0 +1,3 @@
<automotiveApp>
<uses name="media" />
</automotiveApp>