1
0
mirror of https://github.com/TeamNewPipe/NewPipe.git synced 2024-11-22 02:53:09 +01:00

Merge pull request #8678 from Stypox/media-session-ui

Create media session UI and fix player notification
This commit is contained in:
litetex 2022-08-25 17:18:36 +02:00 committed by GitHub
commit 75917c7f61
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 332 additions and 465 deletions

View File

@ -54,7 +54,6 @@ import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.media.AudioManager; import android.media.AudioManager;
import android.util.Log; import android.util.Log;
@ -99,14 +98,13 @@ import org.schabi.newpipe.player.event.PlayerEventListener;
import org.schabi.newpipe.player.event.PlayerServiceEventListener; import org.schabi.newpipe.player.event.PlayerServiceEventListener;
import org.schabi.newpipe.player.helper.AudioReactor; import org.schabi.newpipe.player.helper.AudioReactor;
import org.schabi.newpipe.player.helper.LoadController; import org.schabi.newpipe.player.helper.LoadController;
import org.schabi.newpipe.player.helper.MediaSessionManager;
import org.schabi.newpipe.player.helper.PlayerDataSource; import org.schabi.newpipe.player.helper.PlayerDataSource;
import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.mediaitem.MediaItemTag; import org.schabi.newpipe.player.mediaitem.MediaItemTag;
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi;
import org.schabi.newpipe.player.notification.NotificationPlayerUi; import org.schabi.newpipe.player.notification.NotificationPlayerUi;
import org.schabi.newpipe.player.playback.MediaSourceManager; import org.schabi.newpipe.player.playback.MediaSourceManager;
import org.schabi.newpipe.player.playback.PlaybackListener; import org.schabi.newpipe.player.playback.PlaybackListener;
import org.schabi.newpipe.player.playback.PlayerMediaSession;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.player.resolver.AudioPlaybackResolver; import org.schabi.newpipe.player.resolver.AudioPlaybackResolver;
@ -176,6 +174,7 @@ public final class Player implements PlaybackListener, Listener {
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
public static final int RENDERER_UNAVAILABLE = -1; public static final int RENDERER_UNAVAILABLE = -1;
private static final String PICASSO_PLAYER_THUMBNAIL_TAG = "PICASSO_PLAYER_THUMBNAIL_TAG";
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Playback // Playback
@ -196,7 +195,6 @@ public final class Player implements PlaybackListener, Listener {
private ExoPlayer simpleExoPlayer; private ExoPlayer simpleExoPlayer;
private AudioReactor audioReactor; private AudioReactor audioReactor;
private MediaSessionManager mediaSessionManager;
@NonNull private final DefaultTrackSelector trackSelector; @NonNull private final DefaultTrackSelector trackSelector;
@NonNull private final LoadController loadController; @NonNull private final LoadController loadController;
@ -224,8 +222,8 @@ public final class Player implements PlaybackListener, Listener {
// UIs, listeners and disposables // UIs, listeners and disposables
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@SuppressWarnings("MemberName") // keep the unusual member name @SuppressWarnings({"MemberName", "java:S116"}) // keep the unusual member name
private final PlayerUiList UIs = new PlayerUiList(); private final PlayerUiList UIs;
private BroadcastReceiver broadcastReceiver; private BroadcastReceiver broadcastReceiver;
private IntentFilter intentFilter; private IntentFilter intentFilter;
@ -235,6 +233,11 @@ public final class Player implements PlaybackListener, Listener {
@NonNull private final SerialDisposable progressUpdateDisposable = new SerialDisposable(); @NonNull private final SerialDisposable progressUpdateDisposable = new SerialDisposable();
@NonNull private final CompositeDisposable databaseUpdateDisposable = new CompositeDisposable(); @NonNull private final CompositeDisposable databaseUpdateDisposable = new CompositeDisposable();
// This is the only listener we need for thumbnail loading, since there is always at most only
// one thumbnail being loaded at a time. This field is also here to maintain a strong reference,
// which would otherwise be garbage collected since Picasso holds weak references to targets.
@NonNull private final Target currentThumbnailTarget;
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Utils // Utils
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -265,6 +268,17 @@ public final class Player implements PlaybackListener, Listener {
videoResolver = new VideoPlaybackResolver(context, dataSource, getQualityResolver()); videoResolver = new VideoPlaybackResolver(context, dataSource, getQualityResolver());
audioResolver = new AudioPlaybackResolver(context, dataSource); audioResolver = new AudioPlaybackResolver(context, dataSource);
currentThumbnailTarget = getCurrentThumbnailTarget();
// The UIs added here should always be present. They will be initialized when the player
// reaches the initialization step. Make sure the media session ui is before the
// 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 NotificationPlayerUi(this)
);
} }
private VideoPlaybackResolver.QualityResolver getQualityResolver() { private VideoPlaybackResolver.QualityResolver getQualityResolver() {
@ -431,11 +445,6 @@ public final class Player implements PlaybackListener, Listener {
} }
private void initUIsForCurrentPlayerType() { private void initUIsForCurrentPlayerType() {
//noinspection SimplifyOptionalCallChains
if (!UIs.get(NotificationPlayerUi.class).isPresent()) {
UIs.addAndPrepare(new NotificationPlayerUi(this));
}
if ((UIs.get(MainPlayerUi.class).isPresent() && playerType == PlayerType.MAIN) if ((UIs.get(MainPlayerUi.class).isPresent() && playerType == PlayerType.MAIN)
|| (UIs.get(PopupPlayerUi.class).isPresent() && playerType == PlayerType.POPUP)) { || (UIs.get(PopupPlayerUi.class).isPresent() && playerType == PlayerType.POPUP)) {
// correct UI already in place // correct UI already in place
@ -506,8 +515,6 @@ public final class Player implements PlaybackListener, Listener {
simpleExoPlayer.setHandleAudioBecomingNoisy(true); simpleExoPlayer.setHandleAudioBecomingNoisy(true);
audioReactor = new AudioReactor(context, simpleExoPlayer); audioReactor = new AudioReactor(context, simpleExoPlayer);
mediaSessionManager = new MediaSessionManager(context, simpleExoPlayer,
new PlayerMediaSession(this));
registerBroadcastReceiver(); registerBroadcastReceiver();
@ -558,9 +565,6 @@ public final class Player implements PlaybackListener, Listener {
if (playQueueManager != null) { if (playQueueManager != null) {
playQueueManager.dispose(); playQueueManager.dispose();
} }
if (mediaSessionManager != null) {
mediaSessionManager.dispose();
}
} }
public void destroy() { public void destroy() {
@ -577,7 +581,7 @@ public final class Player implements PlaybackListener, Listener {
databaseUpdateDisposable.clear(); databaseUpdateDisposable.clear();
progressUpdateDisposable.set(null); progressUpdateDisposable.set(null);
PicassoHelper.cancelTag(PicassoHelper.PLAYER_THUMBNAIL_TAG); // cancel thumbnail loading cancelLoadingCurrentThumbnail();
UIs.destroyAll(Object.class); // destroy every UI: obviously every UI extends Object UIs.destroyAll(Object.class); // destroy every UI: obviously every UI extends Object
} }
@ -723,11 +727,6 @@ public final class Player implements PlaybackListener, Listener {
Log.d(TAG, "ACTION_CONFIGURATION_CHANGED received"); Log.d(TAG, "ACTION_CONFIGURATION_CHANGED received");
} }
break; break;
case Intent.ACTION_HEADSET_PLUG: //FIXME
/*notificationManager.cancel(NOTIFICATION_ID);
mediaSessionManager.dispose();
mediaSessionManager.enable(getBaseContext(), basePlayerImpl.simpleExoPlayer);*/
break;
} }
UIs.call(playerUi -> playerUi.onBroadcastReceived(intent)); UIs.call(playerUi -> playerUi.onBroadcastReceived(intent));
@ -756,44 +755,63 @@ public final class Player implements PlaybackListener, Listener {
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
//region Thumbnail loading //region Thumbnail loading
private void initThumbnail(final String url) { private Target getCurrentThumbnailTarget() {
if (DEBUG) { // a Picasso target is just a listener for thumbnail loading events
Log.d(TAG, "Thumbnail - initThumbnail() called with url = [" return new Target() {
+ (url == null ? "null" : url) + "]");
}
if (isNullOrEmpty(url)) {
return;
}
// scale down the notification thumbnail for performance
PicassoHelper.loadScaledDownThumbnail(context, url).into(new Target() {
@Override @Override
public void onBitmapLoaded(final Bitmap bitmap, final Picasso.LoadedFrom from) { public void onBitmapLoaded(final Bitmap bitmap, final Picasso.LoadedFrom from) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "Thumbnail - onLoadingComplete() called with: url = [" + url Log.d(TAG, "Thumbnail - onBitmapLoaded() called with: bitmap = [" + bitmap
+ "], " + "loadedImage = [" + bitmap + " -> " + bitmap.getWidth() + "x" + " -> " + bitmap.getWidth() + "x" + bitmap.getHeight() + "], from = ["
+ bitmap.getHeight() + "], from = [" + from + "]"); + from + "]");
} }
currentThumbnail = bitmap; currentThumbnail = bitmap;
// there is a new thumbnail, so changed the end screen thumbnail, too. // there is a new thumbnail, so e.g. the end screen thumbnail needs to change, too.
UIs.call(playerUi -> playerUi.onThumbnailLoaded(bitmap)); UIs.call(playerUi -> playerUi.onThumbnailLoaded(bitmap));
} }
@Override @Override
public void onBitmapFailed(final Exception e, final Drawable errorDrawable) { public void onBitmapFailed(final Exception e, final Drawable errorDrawable) {
Log.e(TAG, "Thumbnail - onBitmapFailed() called: url = [" + url + "]", e); Log.e(TAG, "Thumbnail - onBitmapFailed() called", e);
currentThumbnail = null; currentThumbnail = null;
// there is a new thumbnail, so e.g. the end screen thumbnail needs to change, too.
UIs.call(playerUi -> playerUi.onThumbnailLoaded(null)); UIs.call(playerUi -> playerUi.onThumbnailLoaded(null));
} }
@Override @Override
public void onPrepareLoad(final Drawable placeHolderDrawable) { public void onPrepareLoad(final Drawable placeHolderDrawable) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "Thumbnail - onPrepareLoad() called: url = [" + url + "]"); Log.d(TAG, "Thumbnail - onPrepareLoad() called");
} }
} }
}); };
}
private void loadCurrentThumbnail(final String url) {
if (DEBUG) {
Log.d(TAG, "Thumbnail - loadCurrentThumbnail() called with url = ["
+ (url == null ? "null" : url) + "]");
}
// first cancel any previous loading
cancelLoadingCurrentThumbnail();
// Unset currentThumbnail, since it is now outdated. This ensures it is not used in media
// session metadata while the new thumbnail is being loaded by Picasso.
currentThumbnail = null;
if (isNullOrEmpty(url)) {
return;
}
// scale down the notification thumbnail for performance
PicassoHelper.loadScaledDownThumbnail(context, url)
.tag(PICASSO_PLAYER_THUMBNAIL_TAG)
.into(currentThumbnailTarget);
}
private void cancelLoadingCurrentThumbnail() {
// cancel the Picasso job associated with the player thumbnail, if any
PicassoHelper.cancelTag(PICASSO_PLAYER_THUMBNAIL_TAG);
} }
//endregion //endregion
@ -1735,18 +1753,9 @@ public final class Player implements PlaybackListener, Listener {
maybeAutoQueueNextStream(info); maybeAutoQueueNextStream(info);
initThumbnail(info.getThumbnailUrl()); loadCurrentThumbnail(info.getThumbnailUrl());
registerStreamViewed(); registerStreamViewed();
final boolean showThumbnail = prefs.getBoolean(
context.getString(R.string.show_thumbnail_key), true);
mediaSessionManager.setMetadata(
getVideoTitle(),
getUploaderName(),
showThumbnail ? Optional.ofNullable(getThumbnail()) : Optional.empty(),
StreamTypeUtil.isLiveStream(info.getStreamType()) ? -1 : info.getDuration()
);
notifyMetadataUpdateToListeners(); notifyMetadataUpdateToListeners();
UIs.call(playerUi -> playerUi.onMetadataChanged(info)); UIs.call(playerUi -> playerUi.onMetadataChanged(info));
} }
@ -1786,10 +1795,6 @@ public final class Player implements PlaybackListener, Listener {
@Nullable @Nullable
public Bitmap getThumbnail() { public Bitmap getThumbnail() {
if (currentThumbnail == null) {
currentThumbnail = BitmapFactory.decodeResource(
context.getResources(), R.drawable.placeholder_thumbnail_video);
}
return currentThumbnail; return currentThumbnail;
} }
//endregion //endregion
@ -2194,10 +2199,6 @@ public final class Player implements PlaybackListener, Listener {
return prefs; return prefs;
} }
public MediaSessionManager getMediaSessionManager() {
return mediaSessionManager;
}
public PlayerType getPlayerType() { public PlayerType getPlayerType() {
return playerType; return playerType;

View File

@ -28,6 +28,7 @@ import android.os.Binder;
import android.os.IBinder; import android.os.IBinder;
import android.util.Log; import android.util.Log;
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
@ -73,9 +74,8 @@ public final class PlayerService extends Service {
} }
player.handleIntent(intent); player.handleIntent(intent);
if (player.getMediaSessionManager() != null) { player.UIs().get(MediaSessionPlayerUi.class)
player.getMediaSessionManager().handleMediaButtonIntent(intent); .ifPresent(ui -> ui.handleMediaButtonIntent(intent));
}
return START_NOT_STICKY; return START_NOT_STICKY;
} }

View File

@ -1,226 +0,0 @@
package org.schabi.newpipe.player.helper;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.util.Log;
import android.view.KeyEvent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.media.session.MediaButtonReceiver;
import com.google.android.exoplayer2.ForwardingPlayer;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.player.mediasession.MediaSessionCallback;
import org.schabi.newpipe.player.mediasession.PlayQueueNavigator;
import java.util.Optional;
public class MediaSessionManager {
private static final String TAG = MediaSessionManager.class.getSimpleName();
public static final boolean DEBUG = MainActivity.DEBUG;
@NonNull
private final MediaSessionCompat mediaSession;
@NonNull
private final MediaSessionConnector sessionConnector;
private int lastTitleHashCode;
private int lastArtistHashCode;
private long lastDuration;
private int lastAlbumArtHashCode;
public MediaSessionManager(@NonNull final Context context,
@NonNull final Player player,
@NonNull final MediaSessionCallback callback) {
mediaSession = new MediaSessionCompat(context, TAG);
mediaSession.setActive(true);
mediaSession.setPlaybackState(new PlaybackStateCompat.Builder()
.setState(PlaybackStateCompat.STATE_NONE, -1, 1)
.setActions(PlaybackStateCompat.ACTION_SEEK_TO
| PlaybackStateCompat.ACTION_PLAY
| PlaybackStateCompat.ACTION_PAUSE // was play and pause now play/pause
| PlaybackStateCompat.ACTION_SKIP_TO_NEXT
| PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
| PlaybackStateCompat.ACTION_SET_REPEAT_MODE
| PlaybackStateCompat.ACTION_STOP)
.build());
sessionConnector = new MediaSessionConnector(mediaSession);
sessionConnector.setQueueNavigator(new PlayQueueNavigator(mediaSession, callback));
sessionConnector.setPlayer(new ForwardingPlayer(player) {
@Override
public void play() {
callback.play();
}
@Override
public void pause() {
callback.pause();
}
});
}
@Nullable
@SuppressWarnings("UnusedReturnValue")
public KeyEvent handleMediaButtonIntent(final Intent intent) {
return MediaButtonReceiver.handleIntent(mediaSession, intent);
}
public MediaSessionCompat.Token getSessionToken() {
return mediaSession.getSessionToken();
}
/**
* sets the Metadata - if required.
*
* @param title {@link MediaMetadataCompat#METADATA_KEY_TITLE}
* @param artist {@link MediaMetadataCompat#METADATA_KEY_ARTIST}
* @param optAlbumArt {@link MediaMetadataCompat#METADATA_KEY_ALBUM_ART}
* @param duration {@link MediaMetadataCompat#METADATA_KEY_DURATION}
* - should be a negative value for unknown durations, e.g. for livestreams
*/
public void setMetadata(@NonNull final String title,
@NonNull final String artist,
@NonNull final Optional<Bitmap> optAlbumArt,
final long duration
) {
if (DEBUG) {
Log.d(TAG, "setMetadata called:"
+ " t: " + title
+ " a: " + artist
+ " thumb: " + (
optAlbumArt.isPresent()
? optAlbumArt.get().hashCode()
: "<none>")
+ " d: " + duration);
}
if (!mediaSession.isActive()) {
if (DEBUG) {
Log.d(TAG, "setMetadata: mediaSession not active - exiting");
}
return;
}
if (!checkIfMetadataShouldBeSet(title, artist, optAlbumArt, duration)) {
if (DEBUG) {
Log.d(TAG, "setMetadata: No update required - exiting");
}
return;
}
if (DEBUG) {
Log.d(TAG, "setMetadata: N_Metadata update:"
+ " t: " + title
+ " a: " + artist
+ " thumb: " + (
optAlbumArt.isPresent()
? optAlbumArt.get().hashCode()
: "<none>")
+ " d: " + duration);
}
final MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder()
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artist)
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration);
if (optAlbumArt.isPresent()) {
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, optAlbumArt.get());
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, optAlbumArt.get());
}
mediaSession.setMetadata(builder.build());
lastTitleHashCode = title.hashCode();
lastArtistHashCode = artist.hashCode();
lastDuration = duration;
optAlbumArt.ifPresent(bitmap -> lastAlbumArtHashCode = bitmap.hashCode());
}
private boolean checkIfMetadataShouldBeSet(
@NonNull final String title,
@NonNull final String artist,
@NonNull final Optional<Bitmap> optAlbumArt,
final long duration
) {
// Check if the values have changed since the last time
if (title.hashCode() != lastTitleHashCode
|| artist.hashCode() != lastArtistHashCode
|| duration != lastDuration
|| (optAlbumArt.isPresent() && optAlbumArt.get().hashCode() != lastAlbumArtHashCode)
) {
if (DEBUG) {
Log.d(TAG,
"checkIfMetadataShouldBeSet: true - reason: changed values since last");
}
return true;
}
// Check if the currently set metadata is valid
if (getMetadataTitle() == null
|| getMetadataArtist() == null
// Note that the duration can be <= 0 for live streams
) {
if (DEBUG) {
if (getMetadataTitle() == null) {
Log.d(TAG,
"N_getMetadataTitle: title == null");
} else if (getMetadataArtist() == null) {
Log.d(TAG,
"N_getMetadataArtist: artist == null");
}
}
return true;
}
// If we got an album art check if the current set AlbumArt is null
if (optAlbumArt.isPresent() && getMetadataAlbumArt() == null) {
if (DEBUG) {
Log.d(TAG, "N_getMetadataAlbumArt: thumb == null");
}
return true;
}
// Default - no update required
return false;
}
@Nullable
private Bitmap getMetadataAlbumArt() {
return mediaSession.getController().getMetadata()
.getBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART);
}
@Nullable
private String getMetadataTitle() {
return mediaSession.getController().getMetadata()
.getString(MediaMetadataCompat.METADATA_KEY_TITLE);
}
@Nullable
private String getMetadataArtist() {
return mediaSession.getController().getMetadata()
.getString(MediaMetadataCompat.METADATA_KEY_ARTIST);
}
/**
* Should be called on player destruction to prevent leakage.
*/
public void dispose() {
sessionConnector.setPlayer(null);
sessionConnector.setQueueNavigator(null);
mediaSession.setActive(false);
mediaSession.release();
}
}

View File

@ -1,21 +0,0 @@
package org.schabi.newpipe.player.mediasession;
import android.support.v4.media.MediaDescriptionCompat;
public interface MediaSessionCallback {
void playPrevious();
void playNext();
void playItemAtIndex(int index);
int getCurrentPlayingIndex();
int getQueueSize();
MediaDescriptionCompat getQueueMetadata(int index);
void play();
void pause();
}

View File

@ -0,0 +1,134 @@
package org.schabi.newpipe.player.mediasession;
import static org.schabi.newpipe.MainActivity.DEBUG;
import android.content.Intent;
import android.graphics.Bitmap;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.media.session.MediaButtonReceiver;
import com.google.android.exoplayer2.ForwardingPlayer;
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
import org.schabi.newpipe.R;
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.ui.PlayerUi;
import org.schabi.newpipe.player.ui.VideoPlayerUi;
import org.schabi.newpipe.util.StreamTypeUtil;
import java.util.Optional;
public class MediaSessionPlayerUi extends PlayerUi {
private static final String TAG = "MediaSessUi";
private MediaSessionCompat mediaSession;
private MediaSessionConnector sessionConnector;
public MediaSessionPlayerUi(@NonNull final Player player) {
super(player);
}
@Override
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());
sessionConnector.setMetadataDeduplicationEnabled(true);
sessionConnector.setMediaMetadataProvider(exoPlayer -> buildMediaMetadata());
}
@Override
public void destroyPlayer() {
super.destroyPlayer();
if (sessionConnector != null) {
sessionConnector.setPlayer(null);
sessionConnector.setQueueNavigator(null);
sessionConnector = null;
}
if (mediaSession != null) {
mediaSession.setActive(false);
mediaSession.release();
mediaSession = null;
}
}
@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();
}
}
public void handleMediaButtonIntent(final Intent intent) {
MediaButtonReceiver.handleIntent(mediaSession, intent);
}
public Optional<MediaSessionCompat.Token> getSessionToken() {
return Optional.ofNullable(mediaSession).map(MediaSessionCompat::getSessionToken);
}
private ForwardingPlayer getForwardingPlayer() {
// ForwardingPlayer means that all media session actions called on this player are
// forwarded directly to the connected exoplayer, except for the overridden methods. So
// override play and pause since our player adds more functionality to them over exoplayer.
return new ForwardingPlayer(player.getExoPlayer()) {
@Override
public void play() {
player.play();
// hide the player controls even if the play command came from the media session
player.UIs().get(VideoPlayerUi.class).ifPresent(ui -> ui.hideControls(0, 0));
}
@Override
public void pause() {
player.pause();
}
};
}
private MediaMetadataCompat buildMediaMetadata() {
if (DEBUG) {
Log.d(TAG, "buildMediaMetadata called");
}
// set title and artist
final MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder()
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, player.getVideoTitle())
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, player.getUploaderName());
// set duration (-1 for livestreams or if unknown, see the METADATA_KEY_DURATION docs)
final long duration = player.getCurrentStreamInfo()
.filter(info -> !StreamTypeUtil.isLiveStream(info.getStreamType()))
.map(info -> info.getDuration() * 1000L)
.orElse(-1L);
builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration);
// set album art, unless the user asked not to, or there is no thumbnail available
final boolean showThumbnail = player.getPrefs().getBoolean(
context.getString(R.string.show_thumbnail_key), true);
Optional.ofNullable(player.getThumbnail())
.filter(bitmap -> showThumbnail)
.ifPresent(bitmap -> {
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, bitmap);
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, bitmap);
});
return builder.build();
}
}

View File

@ -1,106 +1,152 @@
package org.schabi.newpipe.player.mediasession; package org.schabi.newpipe.player.mediasession;
import android.os.Bundle;
import android.os.ResultReceiver;
import android.support.v4.media.session.MediaSessionCompat;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
import com.google.android.exoplayer2.util.Util;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_NEXT; import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_NEXT;
import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS; import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;
import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM; import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM;
import android.net.Uri;
import android.os.Bundle;
import android.os.ResultReceiver;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaSessionCompat;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
import com.google.android.exoplayer2.util.Util;
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
public class PlayQueueNavigator implements MediaSessionConnector.QueueNavigator { public class PlayQueueNavigator implements MediaSessionConnector.QueueNavigator {
public static final int DEFAULT_MAX_QUEUE_SIZE = 10; private static final int MAX_QUEUE_SIZE = 10;
private final MediaSessionCompat mediaSession; private final MediaSessionCompat mediaSession;
private final MediaSessionCallback callback; private final Player player;
private final int maxQueueSize;
private long activeQueueItemId; private long activeQueueItemId;
public PlayQueueNavigator(@NonNull final MediaSessionCompat mediaSession, public PlayQueueNavigator(@NonNull final MediaSessionCompat mediaSession,
@NonNull final MediaSessionCallback callback) { @NonNull final Player player) {
this.mediaSession = mediaSession; this.mediaSession = mediaSession;
this.callback = callback; this.player = player;
this.maxQueueSize = DEFAULT_MAX_QUEUE_SIZE;
this.activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID; this.activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID;
} }
@Override @Override
public long getSupportedQueueNavigatorActions(@Nullable final Player player) { public long getSupportedQueueNavigatorActions(
@Nullable final com.google.android.exoplayer2.Player exoPlayer) {
return ACTION_SKIP_TO_NEXT | ACTION_SKIP_TO_PREVIOUS | ACTION_SKIP_TO_QUEUE_ITEM; return ACTION_SKIP_TO_NEXT | ACTION_SKIP_TO_PREVIOUS | ACTION_SKIP_TO_QUEUE_ITEM;
} }
@Override @Override
public void onTimelineChanged(@NonNull final Player player) { public void onTimelineChanged(@NonNull final com.google.android.exoplayer2.Player exoPlayer) {
publishFloatingQueueWindow(); publishFloatingQueueWindow();
} }
@Override @Override
public void onCurrentMediaItemIndexChanged(@NonNull final Player player) { public void onCurrentMediaItemIndexChanged(
@NonNull final com.google.android.exoplayer2.Player exoPlayer) {
if (activeQueueItemId == MediaSessionCompat.QueueItem.UNKNOWN_ID if (activeQueueItemId == MediaSessionCompat.QueueItem.UNKNOWN_ID
|| player.getCurrentTimeline().getWindowCount() > maxQueueSize) { || exoPlayer.getCurrentTimeline().getWindowCount() > MAX_QUEUE_SIZE) {
publishFloatingQueueWindow(); publishFloatingQueueWindow();
} else if (!player.getCurrentTimeline().isEmpty()) { } else if (!exoPlayer.getCurrentTimeline().isEmpty()) {
activeQueueItemId = player.getCurrentMediaItemIndex(); activeQueueItemId = exoPlayer.getCurrentMediaItemIndex();
} }
} }
@Override @Override
public long getActiveQueueItemId(@Nullable final Player player) { public long getActiveQueueItemId(
return callback.getCurrentPlayingIndex(); @Nullable final com.google.android.exoplayer2.Player exoPlayer) {
return Optional.ofNullable(player.getPlayQueue()).map(PlayQueue::getIndex).orElse(-1);
} }
@Override @Override
public void onSkipToPrevious(@NonNull final Player player) { public void onSkipToPrevious(@NonNull final com.google.android.exoplayer2.Player exoPlayer) {
callback.playPrevious(); player.playPrevious();
} }
@Override @Override
public void onSkipToQueueItem(@NonNull final Player player, final long id) { public void onSkipToQueueItem(@NonNull final com.google.android.exoplayer2.Player exoPlayer,
callback.playItemAtIndex((int) id); final long id) {
if (player.getPlayQueue() != null) {
player.selectQueueItem(player.getPlayQueue().getItem((int) id));
}
} }
@Override @Override
public void onSkipToNext(@NonNull final Player player) { public void onSkipToNext(@NonNull final com.google.android.exoplayer2.Player exoPlayer) {
callback.playNext(); player.playNext();
} }
private void publishFloatingQueueWindow() { private void publishFloatingQueueWindow() {
if (callback.getQueueSize() == 0) { final int windowCount = Optional.ofNullable(player.getPlayQueue())
.map(PlayQueue::size)
.orElse(0);
if (windowCount == 0) {
mediaSession.setQueue(Collections.emptyList()); mediaSession.setQueue(Collections.emptyList());
activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID; activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID;
return; return;
} }
// Yes this is almost a copypasta, got a problem with that? =\ // Yes this is almost a copypasta, got a problem with that? =\
final int windowCount = callback.getQueueSize(); final int currentWindowIndex = player.getPlayQueue().getIndex();
final int currentWindowIndex = callback.getCurrentPlayingIndex(); final int queueSize = Math.min(MAX_QUEUE_SIZE, windowCount);
final int queueSize = Math.min(maxQueueSize, windowCount);
final int startIndex = Util.constrainValue(currentWindowIndex - ((queueSize - 1) / 2), 0, final int startIndex = Util.constrainValue(currentWindowIndex - ((queueSize - 1) / 2), 0,
windowCount - queueSize); windowCount - queueSize);
final List<MediaSessionCompat.QueueItem> queue = new ArrayList<>(); final List<MediaSessionCompat.QueueItem> queue = new ArrayList<>();
for (int i = startIndex; i < startIndex + queueSize; i++) { for (int i = startIndex; i < startIndex + queueSize; i++) {
queue.add(new MediaSessionCompat.QueueItem(callback.getQueueMetadata(i), i)); queue.add(new MediaSessionCompat.QueueItem(getQueueMetadata(i), i));
} }
mediaSession.setQueue(queue); mediaSession.setQueue(queue);
activeQueueItemId = currentWindowIndex; activeQueueItemId = currentWindowIndex;
} }
public MediaDescriptionCompat getQueueMetadata(final int index) {
if (player.getPlayQueue() == null) {
return null;
}
final PlayQueueItem item = player.getPlayQueue().getItem(index);
if (item == null) {
return null;
}
final MediaDescriptionCompat.Builder descBuilder = new MediaDescriptionCompat.Builder()
.setMediaId(String.valueOf(index))
.setTitle(item.getTitle())
.setSubtitle(item.getUploader());
// set additional metadata for A2DP/AVRCP (Audio/Video Bluetooth profiles)
final Bundle additionalMetadata = new Bundle();
additionalMetadata.putString(MediaMetadataCompat.METADATA_KEY_TITLE, item.getTitle());
additionalMetadata.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, item.getUploader());
additionalMetadata
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, item.getDuration() * 1000);
additionalMetadata.putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, index + 1L);
additionalMetadata
.putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, player.getPlayQueue().size());
descBuilder.setExtras(additionalMetadata);
final Uri thumbnailUri = Uri.parse(item.getThumbnailUrl());
if (thumbnailUri != null) {
descBuilder.setIconUri(thumbnailUri);
}
return descBuilder.build();
}
@Override @Override
public boolean onCommand(@NonNull final Player player, public boolean onCommand(@NonNull final com.google.android.exoplayer2.Player exoPlayer,
@NonNull final String command, @NonNull final String command,
@Nullable final Bundle extras, @Nullable final Bundle extras,
@Nullable final ResultReceiver cb) { @Nullable final ResultReceiver cb) {

View File

@ -9,6 +9,7 @@ import android.os.Build;
import android.util.Log; import android.util.Log;
import androidx.annotation.DrawableRes; import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat;
@ -19,11 +20,13 @@ import androidx.core.content.ContextCompat;
import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.player.Player; import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import java.util.List; import java.util.List;
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
import static androidx.media.app.NotificationCompat.MediaStyle;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL; import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE; import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_CLOSE; import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_CLOSE;
@ -101,9 +104,13 @@ public final class NotificationUtil {
player.getContext(), player.getPrefs(), nonNothingSlotCount); player.getContext(), player.getPrefs(), nonNothingSlotCount);
final int[] compactSlots = compactSlotList.stream().mapToInt(Integer::intValue).toArray(); final int[] compactSlots = compactSlotList.stream().mapToInt(Integer::intValue).toArray();
builder.setStyle(new androidx.media.app.NotificationCompat.MediaStyle() final MediaStyle mediaStyle = new MediaStyle().setShowActionsInCompactView(compactSlots);
.setMediaSession(player.getMediaSessionManager().getSessionToken()) player.UIs()
.setShowActionsInCompactView(compactSlots)) .get(MediaSessionPlayerUi.class)
.flatMap(MediaSessionPlayerUi::getSessionToken)
.ifPresent(mediaStyle::setMediaSession);
builder.setStyle(mediaStyle)
.setPriority(NotificationCompat.PRIORITY_HIGH) .setPriority(NotificationCompat.PRIORITY_HIGH)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setCategory(NotificationCompat.CATEGORY_TRANSPORT) .setCategory(NotificationCompat.CATEGORY_TRANSPORT)
@ -133,12 +140,9 @@ public final class NotificationUtil {
notificationBuilder.setContentTitle(player.getVideoTitle()); notificationBuilder.setContentTitle(player.getVideoTitle());
notificationBuilder.setContentText(player.getUploaderName()); notificationBuilder.setContentText(player.getUploaderName());
notificationBuilder.setTicker(player.getVideoTitle()); notificationBuilder.setTicker(player.getVideoTitle());
updateActions(notificationBuilder); updateActions(notificationBuilder);
final boolean showThumbnail = player.getPrefs().getBoolean( setLargeIcon(notificationBuilder);
player.getContext().getString(R.string.show_thumbnail_key), true);
if (showThumbnail) {
setLargeIcon(notificationBuilder);
}
} }
@ -340,17 +344,26 @@ public final class NotificationUtil {
///////////////////////////////////////////////////// /////////////////////////////////////////////////////
private void setLargeIcon(final NotificationCompat.Builder builder) { private void setLargeIcon(final NotificationCompat.Builder builder) {
final boolean showThumbnail = player.getPrefs().getBoolean(
player.getContext().getString(R.string.show_thumbnail_key), true);
final Bitmap thumbnail = player.getThumbnail();
if (thumbnail == null || !showThumbnail) {
// since the builder is reused, make sure the thumbnail is unset if there is not one
builder.setLargeIcon(null);
return;
}
final boolean scaleImageToSquareAspectRatio = player.getPrefs().getBoolean( final boolean scaleImageToSquareAspectRatio = player.getPrefs().getBoolean(
player.getContext().getString(R.string.scale_to_square_image_in_notifications_key), player.getContext().getString(R.string.scale_to_square_image_in_notifications_key),
false); false);
if (scaleImageToSquareAspectRatio) { if (scaleImageToSquareAspectRatio) {
builder.setLargeIcon(getBitmapWithSquareAspectRatio(player.getThumbnail())); builder.setLargeIcon(getBitmapWithSquareAspectRatio(thumbnail));
} else { } else {
builder.setLargeIcon(player.getThumbnail()); builder.setLargeIcon(thumbnail);
} }
} }
private Bitmap getBitmapWithSquareAspectRatio(final Bitmap bitmap) { private Bitmap getBitmapWithSquareAspectRatio(@NonNull final Bitmap bitmap) {
// Find the smaller dimension and then take a center portion of the image that // Find the smaller dimension and then take a center portion of the image that
// has that size. // has that size.
final int w = bitmap.getWidth(); final int w = bitmap.getWidth();

View File

@ -1,99 +0,0 @@
package org.schabi.newpipe.player.playback;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.MediaMetadataCompat;
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.mediasession.MediaSessionCallback;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.player.ui.VideoPlayerUi;
public class PlayerMediaSession implements MediaSessionCallback {
private final Player player;
public PlayerMediaSession(final Player player) {
this.player = player;
}
@Override
public void playPrevious() {
player.playPrevious();
}
@Override
public void playNext() {
player.playNext();
}
@Override
public void playItemAtIndex(final int index) {
if (player.getPlayQueue() == null) {
return;
}
player.selectQueueItem(player.getPlayQueue().getItem(index));
}
@Override
public int getCurrentPlayingIndex() {
if (player.getPlayQueue() == null) {
return -1;
}
return player.getPlayQueue().getIndex();
}
@Override
public int getQueueSize() {
if (player.getPlayQueue() == null) {
return -1;
}
return player.getPlayQueue().size();
}
@Override
public MediaDescriptionCompat getQueueMetadata(final int index) {
if (player.getPlayQueue() == null) {
return null;
}
final PlayQueueItem item = player.getPlayQueue().getItem(index);
if (item == null) {
return null;
}
final MediaDescriptionCompat.Builder descBuilder = new MediaDescriptionCompat.Builder()
.setMediaId(String.valueOf(index))
.setTitle(item.getTitle())
.setSubtitle(item.getUploader());
// set additional metadata for A2DP/AVRCP
final Bundle additionalMetadata = new Bundle();
additionalMetadata.putString(MediaMetadataCompat.METADATA_KEY_TITLE, item.getTitle());
additionalMetadata.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, item.getUploader());
additionalMetadata
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, item.getDuration() * 1000);
additionalMetadata.putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, index + 1);
additionalMetadata
.putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, player.getPlayQueue().size());
descBuilder.setExtras(additionalMetadata);
final Uri thumbnailUri = Uri.parse(item.getThumbnailUrl());
if (thumbnailUri != null) {
descBuilder.setIconUri(thumbnailUri);
}
return descBuilder.build();
}
@Override
public void play() {
player.play();
// hide the player controls even if the play command came from the media session
player.UIs().get(VideoPlayerUi.class).ifPresent(playerUi -> playerUi.hideControls(0, 0));
}
@Override
public void pause() {
player.pause();
}
}

View File

@ -29,7 +29,8 @@ public abstract class PlayerUi {
@NonNull protected final Player player; @NonNull protected final Player player;
/** /**
* @param player the player instance that will be usable throughout the lifetime of this UI * @param player the player instance that will be usable throughout the lifetime of this UI; its
* context should already have been initialized
*/ */
protected PlayerUi(@NonNull final Player player) { protected PlayerUi(@NonNull final Player player) {
this.context = player.getContext(); this.context = player.getContext();

View File

@ -8,6 +8,19 @@ import java.util.function.Consumer;
public final class PlayerUiList { public final class PlayerUiList {
final List<PlayerUi> playerUis = new ArrayList<>(); final List<PlayerUi> playerUis = new ArrayList<>();
/**
* Creates a {@link PlayerUiList} starting with the provided player uis. The provided player uis
* will not be prepared like those passed to {@link #addAndPrepare(PlayerUi)}, because when
* the {@link PlayerUiList} constructor is called, the player is still not running and it
* wouldn't make sense to initialize uis then. Instead the player will initialize them by doing
* proper calls to {@link #call(Consumer)}.
*
* @param initialPlayerUis the player uis this list should start with; the order will be kept
*/
public PlayerUiList(final PlayerUi... initialPlayerUis) {
playerUis.addAll(List.of(initialPlayerUis));
}
/** /**
* Adds the provided player ui to the list and calls on it the initialization functions that * Adds the provided player ui to the list and calls on it the initialization functions that
* apply based on the current player state. The preparation step needs to be done since when UIs * apply based on the current player state. The preparation step needs to be done since when UIs
@ -67,11 +80,11 @@ public final class PlayerUiList {
} }
/** /**
* Calls the provided consumer on all player UIs in the list. * Calls the provided consumer on all player UIs in the list, in order of addition.
* @param consumer the consumer to call with player UIs * @param consumer the consumer to call with player UIs
*/ */
public void call(final Consumer<PlayerUi> consumer) { public void call(final Consumer<PlayerUi> consumer) {
//noinspection SimplifyStreamApiCallChains //noinspection SimplifyStreamApiCallChains
playerUis.stream().forEach(consumer); playerUis.stream().forEachOrdered(consumer);
} }
} }

View File

@ -1,10 +1,12 @@
package org.schabi.newpipe.util; package org.schabi.newpipe.util;
import static org.schabi.newpipe.MainActivity.DEBUG;
import static org.schabi.newpipe.extractor.utils.Utils.isBlank; import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.util.Log;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -24,7 +26,7 @@ import java.util.concurrent.TimeUnit;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
public final class PicassoHelper { public final class PicassoHelper {
public static final String PLAYER_THUMBNAIL_TAG = "PICASSO_PLAYER_THUMBNAIL_TAG"; private static final String TAG = PicassoHelper.class.getSimpleName();
private static final String PLAYER_THUMBNAIL_TRANSFORMATION_KEY = private static final String PLAYER_THUMBNAIL_TRANSFORMATION_KEY =
"PICASSO_PLAYER_THUMBNAIL_TRANSFORMATION_KEY"; "PICASSO_PLAYER_THUMBNAIL_TRANSFORMATION_KEY";
@ -125,10 +127,13 @@ public final class PicassoHelper {
public static RequestCreator loadScaledDownThumbnail(final Context context, final String url) { public static RequestCreator loadScaledDownThumbnail(final Context context, final String url) {
// scale down the notification thumbnail for performance // scale down the notification thumbnail for performance
return PicassoHelper.loadThumbnail(url) return PicassoHelper.loadThumbnail(url)
.tag(PLAYER_THUMBNAIL_TAG)
.transform(new Transformation() { .transform(new Transformation() {
@Override @Override
public Bitmap transform(final Bitmap source) { public Bitmap transform(final Bitmap source) {
if (DEBUG) {
Log.d(TAG, "Thumbnail - transform() called");
}
final float notificationThumbnailWidth = Math.min( final float notificationThumbnailWidth = Math.min(
context.getResources() context.getResources()
.getDimension(R.dimen.player_notification_thumbnail_width), .getDimension(R.dimen.player_notification_thumbnail_width),