[Notification] Fixed and improved playing notification

This commit is contained in:
Prathamesh More 2021-12-05 14:52:03 +05:30
parent ba9c928588
commit 1df50491fc
4 changed files with 482 additions and 443 deletions

View file

@ -25,6 +25,7 @@ import static code.name.monkey.retromusic.ConstantsKt.CROSS_FADE_DURATION;
import static code.name.monkey.retromusic.ConstantsKt.TOGGLE_HEADSET; import static code.name.monkey.retromusic.ConstantsKt.TOGGLE_HEADSET;
import static code.name.monkey.retromusic.service.AudioFader.startFadeAnimator; import static code.name.monkey.retromusic.service.AudioFader.startFadeAnimator;
import android.app.NotificationManager;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetManager;
import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothDevice;
@ -34,6 +35,7 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.ServiceInfo;
import android.database.ContentObserver; import android.database.ContentObserver;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.Point; import android.graphics.Point;
@ -106,6 +108,7 @@ import code.name.monkey.retromusic.util.PreferenceUtil;
import code.name.monkey.retromusic.util.RetroUtil; import code.name.monkey.retromusic.util.RetroUtil;
import code.name.monkey.retromusic.volume.AudioVolumeObserver; import code.name.monkey.retromusic.volume.AudioVolumeObserver;
import code.name.monkey.retromusic.volume.OnAudioVolumeChangedListener; import code.name.monkey.retromusic.volume.OnAudioVolumeChangedListener;
import kotlin.Unit;
/** /**
* @author Karim Abou Zeid (kabouzeid), Andrew Neal * @author Karim Abou Zeid (kabouzeid), Andrew Neal
@ -280,7 +283,8 @@ public class MusicService extends MediaBrowserServiceCompat
new BroadcastReceiver() { new BroadcastReceiver() {
@Override @Override
public void onReceive(final Context context, final Intent intent) { public void onReceive(final Context context, final Intent intent) {
updateNotification(); playingNotification.updateFavorite(getCurrentSong(), MusicService.this::startForegroundOrNotify);
startForegroundOrNotify();
} }
}; };
private final BroadcastReceiver lockScreenReceiver = private final BroadcastReceiver lockScreenReceiver =
@ -363,6 +367,8 @@ public class MusicService extends MediaBrowserServiceCompat
private ThrottledSeekHandler throttledSeekHandler; private ThrottledSeekHandler throttledSeekHandler;
private Handler uiThreadHandler; private Handler uiThreadHandler;
private PowerManager.WakeLock wakeLock; private PowerManager.WakeLock wakeLock;
private NotificationManager notificationManager;
private boolean isForeground = false;
private static Bitmap copy(Bitmap bitmap) { private static Bitmap copy(Bitmap bitmap) {
Bitmap.Config config = bitmap.getConfig(); Bitmap.Config config = bitmap.getConfig();
@ -424,6 +430,10 @@ public class MusicService extends MediaBrowserServiceCompat
registerReceiver(updateFavoriteReceiver, new IntentFilter(FAVORITE_STATE_CHANGED)); registerReceiver(updateFavoriteReceiver, new IntentFilter(FAVORITE_STATE_CHANGED));
registerReceiver(lockScreenReceiver, new IntentFilter(Intent.ACTION_SCREEN_OFF)); registerReceiver(lockScreenReceiver, new IntentFilter(Intent.ACTION_SCREEN_OFF));
setSessionToken(mediaSession.getSessionToken());
if (VersionUtils.INSTANCE.hasMarshmallow()) {
notificationManager = getSystemService(NotificationManager.class);
}
initNotification(); initNotification();
mediaStoreObserver = new MediaStoreObserver(this, playerHandler); mediaStoreObserver = new MediaStoreObserver(this, playerHandler);
@ -474,7 +484,6 @@ public class MusicService extends MediaBrowserServiceCompat
mPackageValidator = new PackageValidator(this, R.xml.allowed_media_browser_callers); mPackageValidator = new PackageValidator(this, R.xml.allowed_media_browser_callers);
mMusicProvider.setMusicService(this); mMusicProvider.setMusicService(this);
setSessionToken(mediaSession.getSessionToken());
} }
@Override @Override
@ -772,11 +781,10 @@ public class MusicService extends MediaBrowserServiceCompat
public void initNotification() { public void initNotification() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
&& !PreferenceUtil.INSTANCE.isClassicNotification()) { && !PreferenceUtil.INSTANCE.isClassicNotification()) {
playingNotification = new PlayingNotificationImpl(); playingNotification = PlayingNotificationImpl.Companion.from(this, notificationManager, mediaSession);
} else { } else {
playingNotification = new PlayingNotificationOreo(); playingNotification = PlayingNotificationOreo.Companion.from(this, notificationManager);
} }
playingNotification.init(this);
} }
public boolean isLastTrack() { public boolean isLastTrack() {
@ -844,7 +852,8 @@ public class MusicService extends MediaBrowserServiceCompat
// Request from an untrusted package: return an empty browser root // Request from an untrusted package: return an empty browser root
return new BrowserRoot(AutoMediaIDHelper.MEDIA_ID_EMPTY_ROOT, null); return new BrowserRoot(AutoMediaIDHelper.MEDIA_ID_EMPTY_ROOT, null);
} else { } else {
/** By default return the browsable root. Treat the EXTRA_RECENT flag as a special case /**
* By default return the browsable root. Treat the EXTRA_RECENT flag as a special case
* and return the recent root instead. * and return the recent root instead.
*/ */
boolean isRecentRequest = false; boolean isRecentRequest = false;
@ -889,7 +898,7 @@ public class MusicService extends MediaBrowserServiceCompat
/* Switch to MultiPlayer if Crossfade duration is 0 and /* Switch to MultiPlayer if Crossfade duration is 0 and
Playback is not an instance of MultiPlayer */ Playback is not an instance of MultiPlayer */
if (playback != null) if (playback != null)
playback.setCrossFadeDuration(PreferenceUtil.INSTANCE.getCrossFadeDuration()); playback.setCrossFadeDuration(PreferenceUtil.INSTANCE.getCrossFadeDuration());
if (!(playback instanceof MultiPlayer) && PreferenceUtil.INSTANCE.getCrossFadeDuration() == 0) { if (!(playback instanceof MultiPlayer) && PreferenceUtil.INSTANCE.getCrossFadeDuration() == 0) {
if (playback != null) { if (playback != null) {
playback.release(); playback.release();
@ -1049,6 +1058,7 @@ public class MusicService extends MediaBrowserServiceCompat
} }
public void pause() { public void pause() {
Log.i(TAG, "Paused");
pausedByTransientLossOfFocus = false; pausedByTransientLossOfFocus = false;
if (playback != null && playback.isPlaying()) { if (playback != null && playback.isPlaying()) {
startFadeAnimator(playback, false, () -> { startFadeAnimator(playback, false, () -> {
@ -1170,7 +1180,8 @@ public class MusicService extends MediaBrowserServiceCompat
public void quit() { public void quit() {
pause(); pause();
playingNotification.stop(); stopForeground(true);
notificationManager.cancel(PlayingNotification.NOTIFICATION_ID);
closeAudioEffectSession(); closeAudioEffectSession();
getAudioManager().abandonAudioFocus(audioFocusListener); getAudioManager().abandonAudioFocus(audioFocusListener);
@ -1333,7 +1344,8 @@ public class MusicService extends MediaBrowserServiceCompat
public void updateNotification() { public void updateNotification() {
if (playingNotification != null && getCurrentSong().getId() != -1) { if (playingNotification != null && getCurrentSong().getId() != -1) {
playingNotification.update(); stopForegroundAndNotification();
initNotification();
} }
} }
@ -1407,17 +1419,19 @@ public class MusicService extends MediaBrowserServiceCompat
private void handleChangeInternal(@NonNull final String what) { private void handleChangeInternal(@NonNull final String what) {
switch (what) { switch (what) {
case PLAY_STATE_CHANGED: case PLAY_STATE_CHANGED:
updateNotification();
updateMediaSessionPlaybackState(); updateMediaSessionPlaybackState();
final boolean isPlaying = isPlaying(); final boolean isPlaying = isPlaying();
if (!isPlaying && getSongProgressMillis() > 0) { if (!isPlaying && getSongProgressMillis() > 0) {
savePositionInTrack(); savePositionInTrack();
} }
songPlayCountHelper.notifyPlayStateChanged(isPlaying); songPlayCountHelper.notifyPlayStateChanged(isPlaying);
playingNotification.setPlaying(isPlaying);
startForegroundOrNotify();
break; break;
case FAVORITE_STATE_CHANGED: case FAVORITE_STATE_CHANGED:
playingNotification.updateFavorite(getCurrentSong(), this::startForegroundOrNotify);
case META_CHANGED: case META_CHANGED:
updateNotification(); playingNotification.updateMetadata(getCurrentSong(), this::startForegroundOrNotify);
updateMediaSessionMetaData(); updateMediaSessionMetaData();
updateMediaSessionPlaybackState(); updateMediaSessionPlaybackState();
savePosition(); savePosition();
@ -1435,12 +1449,41 @@ public class MusicService extends MediaBrowserServiceCompat
if (playingQueue.size() > 0) { if (playingQueue.size() > 0) {
prepareNext(); prepareNext();
} else { } else {
playingNotification.stop(); stopForegroundAndNotification();
} }
break; break;
} }
} }
private Unit startForegroundOrNotify() {
if (!isForeground) {
// Specify that this is a media service, if supported.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(
PlayingNotification.NOTIFICATION_ID, playingNotification.build(),
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
);
} else {
startForeground(PlayingNotification.NOTIFICATION_ID, playingNotification.build());
}
isForeground = true;
} else {
// If we are already in foreground just update the notification
notificationManager.notify(
PlayingNotification.NOTIFICATION_ID, playingNotification.build()
);
}
return Unit.INSTANCE;
}
private void stopForegroundAndNotification() {
stopForeground(true);
notificationManager.cancel(PlayingNotification.NOTIFICATION_ID);
isForeground = false;
}
private boolean openCurrent() { private boolean openCurrent() {
synchronized (this) { synchronized (this) {
try { try {
@ -1600,8 +1643,8 @@ public class MusicService extends MediaBrowserServiceCompat
mediaButtonIntent.setComponent(mediaButtonReceiverComponentName); mediaButtonIntent.setComponent(mediaButtonReceiverComponentName);
PendingIntent mediaButtonReceiverPendingIntent; PendingIntent mediaButtonReceiverPendingIntent;
mediaButtonReceiverPendingIntent = PendingIntent.getBroadcast(getApplicationContext(), 0, mediaButtonIntent, mediaButtonReceiverPendingIntent = PendingIntent.getBroadcast(getApplicationContext(), 0, mediaButtonIntent,
VersionUtils.INSTANCE.hasMarshmallow() ? PendingIntent.FLAG_IMMUTABLE : 0); VersionUtils.INSTANCE.hasMarshmallow() ? PendingIntent.FLAG_IMMUTABLE : 0);
mediaSession = new MediaSessionCompat( mediaSession = new MediaSessionCompat(
this, this,

View file

@ -15,95 +15,51 @@
package code.name.monkey.retromusic.service.notification package code.name.monkey.retromusic.service.notification
import android.app.Notification
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.content.Context.NOTIFICATION_SERVICE import android.content.Context
import android.content.pm.ServiceInfo
import android.os.Build
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import code.name.monkey.retromusic.R import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.service.MusicService import code.name.monkey.retromusic.model.Song
abstract class PlayingNotification { abstract class PlayingNotification(context: Context) :
protected lateinit var service: MusicService NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID) {
protected var stopped: Boolean = false
private var notifyMode = NOTIFY_MODE_BACKGROUND
private var notificationManager: NotificationManager? = null
abstract fun updateMetadata(song: Song, onUpdate: () -> Unit)
@Synchronized abstract fun setPlaying(isPlaying: Boolean)
fun init(service: MusicService) {
this.service = service
notificationManager = service.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createNotificationChannel()
}
}
abstract fun update() abstract fun updateFavorite(song: Song, onUpdate: () -> Unit)
@Synchronized
fun stop() {
stopped = true
service.stopForeground(true)
notificationManager!!.cancel(NOTIFICATION_ID)
}
internal fun updateNotifyModeAndPostNotification(notification: Notification) {
val newNotifyMode: Int = if (service.isPlaying) {
NOTIFY_MODE_FOREGROUND
} else {
NOTIFY_MODE_BACKGROUND
}
if (notifyMode != newNotifyMode && newNotifyMode == NOTIFY_MODE_BACKGROUND) {
service.stopForeground(false)
}
if (newNotifyMode == NOTIFY_MODE_FOREGROUND) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
service.startForeground(
NOTIFICATION_ID,
notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
)
} else {
service.startForeground(NOTIFICATION_ID,notification)
}
} else if (newNotifyMode == NOTIFY_MODE_BACKGROUND) {
notificationManager!!.notify(NOTIFICATION_ID, notification)
}
notifyMode = newNotifyMode
}
@RequiresApi(26)
private fun createNotificationChannel() {
var notificationChannel: NotificationChannel? = notificationManager!!
.getNotificationChannel(NOTIFICATION_CHANNEL_ID)
if (notificationChannel == null) {
notificationChannel = NotificationChannel(
NOTIFICATION_CHANNEL_ID,
service.getString(R.string.playing_notification_name),
NotificationManager.IMPORTANCE_LOW
)
notificationChannel.description =
service.getString(R.string.playing_notification_description)
notificationChannel.enableLights(false)
notificationChannel.enableVibration(false)
notificationChannel.setShowBadge(false)
notificationManager!!.createNotificationChannel(notificationChannel)
}
}
companion object { companion object {
const val NOTIFICATION_CONTROLS_SIZE_MULTIPLIER = 1.0f const val NOTIFICATION_CONTROLS_SIZE_MULTIPLIER = 1.0f
internal const val NOTIFICATION_CHANNEL_ID = "playing_notification" internal const val NOTIFICATION_CHANNEL_ID = "playing_notification"
private const val NOTIFICATION_ID = 1 const val NOTIFICATION_ID = 1
private const val NOTIFY_MODE_FOREGROUND = 1
private const val NOTIFY_MODE_BACKGROUND = 0
@RequiresApi(26)
fun createNotificationChannel(
context: Context,
notificationManager: NotificationManager
) {
var notificationChannel: NotificationChannel? = notificationManager
.getNotificationChannel(NOTIFICATION_CHANNEL_ID)
if (notificationChannel == null) {
notificationChannel = NotificationChannel(
NOTIFICATION_CHANNEL_ID,
context.getString(R.string.playing_notification_name),
NotificationManager.IMPORTANCE_LOW
)
notificationChannel.description =
context.getString(R.string.playing_notification_description)
notificationChannel.enableLights(false)
notificationChannel.enableVibration(false)
notificationChannel.setShowBadge(false)
notificationManager.createNotificationChannel(notificationChannel)
}
}
} }
} }

View file

@ -14,14 +14,16 @@
package code.name.monkey.retromusic.service.notification package code.name.monkey.retromusic.service.notification
import android.annotation.SuppressLint
import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.content.ComponentName import android.content.ComponentName
import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.Build import android.os.Build
import android.support.v4.media.session.MediaSessionCompat
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat
import androidx.media.app.NotificationCompat.MediaStyle import androidx.media.app.NotificationCompat.MediaStyle
@ -33,180 +35,196 @@ import code.name.monkey.retromusic.db.toSongEntity
import code.name.monkey.retromusic.glide.GlideApp import code.name.monkey.retromusic.glide.GlideApp
import code.name.monkey.retromusic.glide.RetroGlideExtension import code.name.monkey.retromusic.glide.RetroGlideExtension
import code.name.monkey.retromusic.glide.palette.BitmapPaletteWrapper import code.name.monkey.retromusic.glide.palette.BitmapPaletteWrapper
import code.name.monkey.retromusic.model.Song
import code.name.monkey.retromusic.service.MusicService import code.name.monkey.retromusic.service.MusicService
import code.name.monkey.retromusic.service.MusicService.* import code.name.monkey.retromusic.service.MusicService.*
import code.name.monkey.retromusic.util.MusicUtil import code.name.monkey.retromusic.util.MusicUtil
import code.name.monkey.retromusic.util.PreferenceUtil import code.name.monkey.retromusic.util.PreferenceUtil
import code.name.monkey.retromusic.util.RetroColorUtil import code.name.monkey.retromusic.util.RetroColorUtil
import com.bumptech.glide.Glide import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.target.SimpleTarget
import com.bumptech.glide.request.target.Target
import com.bumptech.glide.request.transition.Transition import com.bumptech.glide.request.transition.Transition
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent import kotlinx.coroutines.withContext
class PlayingNotificationImpl : PlayingNotification(), KoinComponent { @SuppressLint("RestrictedApi")
private var target: Target<BitmapPaletteWrapper>? = null class PlayingNotificationImpl(
val context: Context,
mediaSessionToken: MediaSessionCompat.Token
) : PlayingNotification(context) {
@Synchronized init {
override fun update() { val action = Intent(context, MainActivity::class.java)
stopped = false action.putExtra(MainActivity.EXPAND_PANEL, PreferenceUtil.isExpandPanel)
GlobalScope.launch { action.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
val song = service.currentSong val clickIntent =
PendingIntent.getActivity(
context,
0,
action,
PendingIntent.FLAG_UPDATE_CURRENT or if (VersionUtils.hasMarshmallow())
PendingIntent.FLAG_IMMUTABLE
else 0
)
val serviceName = ComponentName(context, MusicService::class.java)
val intent = Intent(ACTION_QUIT)
intent.component = serviceName
val deleteIntent = PendingIntent.getService(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or (if (VersionUtils.hasMarshmallow())
PendingIntent.FLAG_IMMUTABLE
else 0)
)
val toggleFavorite = buildFavoriteAction(false)
val playPauseAction = buildPlayAction(true)
val previousAction = NotificationCompat.Action(
R.drawable.ic_skip_previous_round_white_32dp,
context.getString(R.string.action_previous),
retrievePlaybackAction(ACTION_REWIND)
)
val nextAction = NotificationCompat.Action(
R.drawable.ic_skip_next_round_white_32dp,
context.getString(R.string.action_next),
retrievePlaybackAction(ACTION_SKIP)
)
val dismissAction = NotificationCompat.Action(
R.drawable.ic_close,
context.getString(R.string.customactivityoncrash_error_activity_error_details_close),
retrievePlaybackAction(ACTION_QUIT)
)
setSmallIcon(R.drawable.ic_notification)
setContentIntent(clickIntent)
setDeleteIntent(deleteIntent)
setShowWhen(false)
addAction(toggleFavorite)
addAction(previousAction)
addAction(playPauseAction)
addAction(nextAction)
addAction(dismissAction)
setStyle(
MediaStyle()
.setMediaSession(mediaSessionToken)
.setShowActionsInCompactView(1, 2, 3)
)
setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
if (Build.VERSION.SDK_INT <=
Build.VERSION_CODES.O && PreferenceUtil.isColoredNotification
) {
this.color = color
}
}
override fun updateMetadata(song: Song, onUpdate: () -> Unit) {
setContentTitle(song.title)
setContentText(
HtmlCompat.fromHtml(
"<b>" + song.albumName + "</b>",
HtmlCompat.FROM_HTML_MODE_LEGACY
)
)
val bigNotificationImageSize = context.resources
.getDimensionPixelSize(R.dimen.notification_big_image_size)
GlideApp.with(context).asBitmapPalette().songCoverOptions(song)
.load(RetroGlideExtension.getSongModel(song))
//.checkIgnoreMediaStore()
.centerCrop()
.into(object : CustomTarget<BitmapPaletteWrapper>(
bigNotificationImageSize,
bigNotificationImageSize
) {
override fun onResourceReady(
resource: BitmapPaletteWrapper,
transition: Transition<in BitmapPaletteWrapper>?
) {
setLargeIcon(
resource.bitmap
)
if (Build.VERSION.SDK_INT <=
Build.VERSION_CODES.O && PreferenceUtil.isColoredNotification
) {
color = RetroColorUtil.getColor(resource.palette, Color.TRANSPARENT)
}
onUpdate()
}
override fun onLoadFailed(errorDrawable: Drawable?) {
super.onLoadFailed(errorDrawable)
setLargeIcon(null)
onUpdate()
}
override fun onLoadCleared(placeholder: Drawable?) {
setLargeIcon(null)
onUpdate()
}
})
}
private fun buildPlayAction(isPlaying: Boolean): NotificationCompat.Action {
val playButtonResId =
if (isPlaying) R.drawable.ic_pause_white_48dp else R.drawable.ic_play_arrow_white_48dp
return NotificationCompat.Action.Builder(
playButtonResId,
context.getString(R.string.action_play_pause),
retrievePlaybackAction(ACTION_TOGGLE_PAUSE)
).build()
}
private fun buildFavoriteAction(isFavorite: Boolean): NotificationCompat.Action {
val favoriteResId =
if (isFavorite) R.drawable.ic_favorite else R.drawable.ic_favorite_border
return NotificationCompat.Action.Builder(
favoriteResId,
context.getString(R.string.action_toggle_favorite),
retrievePlaybackAction(TOGGLE_FAVORITE)
).build()
}
override fun setPlaying(isPlaying: Boolean) {
mActions[2] = buildPlayAction(isPlaying)
}
override fun updateFavorite(song: Song, onUpdate: () -> Unit) {
GlobalScope.launch(Dispatchers.IO) {
val playlist: PlaylistEntity = MusicUtil.repository.favoritePlaylist() val playlist: PlaylistEntity = MusicUtil.repository.favoritePlaylist()
val isPlaying = service.isPlaying
val isFavorite = if (playlist != null) { val isFavorite = if (playlist != null) {
val songEntity = song.toSongEntity(playlist.playListId) val songEntity = song.toSongEntity(playlist.playListId)
MusicUtil.repository.isFavoriteSong(songEntity).isNotEmpty() MusicUtil.repository.isFavoriteSong(songEntity).isNotEmpty()
} else false } else false
withContext(Dispatchers.Main) {
val playButtonResId = mActions[0] = buildFavoriteAction(isFavorite)
if (isPlaying) R.drawable.ic_pause_white_48dp else R.drawable.ic_play_arrow_white_48dp onUpdate()
val favoriteResId =
if (isFavorite) R.drawable.ic_favorite else R.drawable.ic_favorite_border
val action = Intent(service, MainActivity::class.java)
action.putExtra(MainActivity.EXPAND_PANEL, PreferenceUtil.isExpandPanel)
action.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
val clickIntent =
PendingIntent.getActivity(
service,
0,
action,
PendingIntent.FLAG_UPDATE_CURRENT or if (VersionUtils.hasMarshmallow())
PendingIntent.FLAG_IMMUTABLE
else 0
)
val serviceName = ComponentName(service, MusicService::class.java)
val intent = Intent(ACTION_QUIT)
intent.component = serviceName
val deleteIntent = PendingIntent.getService(
service,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or (if (VersionUtils.hasMarshmallow())
PendingIntent.FLAG_IMMUTABLE
else 0)
)
val bigNotificationImageSize = service.resources
.getDimensionPixelSize(R.dimen.notification_big_image_size)
service.runOnUiThread {
if (target != null) {
Glide.with(service).clear(target)
}
target = GlideApp.with(service).asBitmapPalette().songCoverOptions(song)
.load(RetroGlideExtension.getSongModel(song))
//.checkIgnoreMediaStore()
.centerCrop()
.into(object : SimpleTarget<BitmapPaletteWrapper>(
bigNotificationImageSize,
bigNotificationImageSize
) {
override fun onResourceReady(
resource: BitmapPaletteWrapper,
transition: Transition<in BitmapPaletteWrapper>?
) {
update(
resource.bitmap,
RetroColorUtil.getColor(resource.palette, Color.TRANSPARENT)
)
}
override fun onLoadFailed(errorDrawable: Drawable?) {
super.onLoadFailed(errorDrawable)
update(null, Color.TRANSPARENT)
}
fun update(bitmap: Bitmap?, color: Int) {
var bitmapFinal = bitmap
if (bitmapFinal == null) {
bitmapFinal = BitmapFactory.decodeResource(
service.resources,
R.drawable.default_audio_art
)
}
val toggleFavorite = NotificationCompat.Action(
favoriteResId,
service.getString(R.string.action_toggle_favorite),
retrievePlaybackAction(TOGGLE_FAVORITE)
)
val playPauseAction = NotificationCompat.Action(
playButtonResId,
service.getString(R.string.action_play_pause),
retrievePlaybackAction(ACTION_TOGGLE_PAUSE)
)
val previousAction = NotificationCompat.Action(
R.drawable.ic_skip_previous_round_white_32dp,
service.getString(R.string.action_previous),
retrievePlaybackAction(ACTION_REWIND)
)
val nextAction = NotificationCompat.Action(
R.drawable.ic_skip_next_round_white_32dp,
service.getString(R.string.action_next),
retrievePlaybackAction(ACTION_SKIP)
)
val builder = NotificationCompat.Builder(
service,
NOTIFICATION_CHANNEL_ID
)
.setSmallIcon(R.drawable.ic_notification)
.setLargeIcon(bitmapFinal)
.setContentIntent(clickIntent)
.setDeleteIntent(deleteIntent)
.setContentTitle(
HtmlCompat.fromHtml(
"<b>" + song.title + "</b>",
HtmlCompat.FROM_HTML_MODE_LEGACY
)
)
.setContentText(song.artistName)
.setSubText(
HtmlCompat.fromHtml(
"<b>" + song.albumName + "</b>",
HtmlCompat.FROM_HTML_MODE_LEGACY
)
)
.setOngoing(isPlaying)
.setShowWhen(false)
.addAction(toggleFavorite)
.addAction(previousAction)
.addAction(playPauseAction)
.addAction(nextAction)
builder.setStyle(
MediaStyle()
.setMediaSession(service.mediaSession.sessionToken)
.setShowActionsInCompactView(1, 2, 3)
)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
if (Build.VERSION.SDK_INT <=
Build.VERSION_CODES.O && PreferenceUtil.isColoredNotification
) {
builder.color = color
}
if (stopped) {
return // notification has been stopped before loading was finished
}
updateNotifyModeAndPostNotification(builder.build())
}
})
} }
} }
} }
private fun retrievePlaybackAction(action: String): PendingIntent { private fun retrievePlaybackAction(action: String): PendingIntent {
val serviceName = ComponentName(service, MusicService::class.java) val serviceName = ComponentName(context, MusicService::class.java)
val intent = Intent(action) val intent = Intent(action)
intent.component = serviceName intent.component = serviceName
return PendingIntent.getService( return PendingIntent.getService(
service, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or
if (VersionUtils.hasMarshmallow()) PendingIntent.FLAG_IMMUTABLE if (VersionUtils.hasMarshmallow()) PendingIntent.FLAG_IMMUTABLE
else 0 else 0
) )
} }
companion object {
fun from(
context: Context,
notificationManager: NotificationManager,
mediaSession: MediaSessionCompat
): PlayingNotification {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createNotificationChannel(context, notificationManager)
}
return PlayingNotificationImpl(context, mediaSession.sessionToken)
}
}
} }

View file

@ -14,6 +14,8 @@
package code.name.monkey.retromusic.service.notification package code.name.monkey.retromusic.service.notification
import android.annotation.SuppressLint
import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
@ -21,6 +23,7 @@ import android.content.Intent
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.Build
import android.widget.RemoteViews import android.widget.RemoteViews
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import code.name.monkey.appthemehelper.util.ATHUtil.resolveColor import code.name.monkey.appthemehelper.util.ATHUtil.resolveColor
@ -29,6 +32,7 @@ import code.name.monkey.appthemehelper.util.MaterialValueHelper
import code.name.monkey.appthemehelper.util.VersionUtils import code.name.monkey.appthemehelper.util.VersionUtils
import code.name.monkey.retromusic.R import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.activities.MainActivity import code.name.monkey.retromusic.activities.MainActivity
import code.name.monkey.retromusic.extensions.surfaceColor
import code.name.monkey.retromusic.glide.GlideApp import code.name.monkey.retromusic.glide.GlideApp
import code.name.monkey.retromusic.glide.RetroGlideExtension import code.name.monkey.retromusic.glide.RetroGlideExtension
import code.name.monkey.retromusic.glide.palette.BitmapPaletteWrapper import code.name.monkey.retromusic.glide.palette.BitmapPaletteWrapper
@ -39,224 +43,231 @@ import code.name.monkey.retromusic.util.PreferenceUtil
import code.name.monkey.retromusic.util.RetroUtil import code.name.monkey.retromusic.util.RetroUtil
import code.name.monkey.retromusic.util.RetroUtil.createBitmap import code.name.monkey.retromusic.util.RetroUtil.createBitmap
import code.name.monkey.retromusic.util.color.MediaNotificationProcessor import code.name.monkey.retromusic.util.color.MediaNotificationProcessor
import com.bumptech.glide.Glide import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.target.SimpleTarget
import com.bumptech.glide.request.target.Target
import com.bumptech.glide.request.transition.Transition import com.bumptech.glide.request.transition.Transition
/** /**
* @author Hemanth S (h4h13). * @author Hemanth S (h4h13).
*/ */
class PlayingNotificationOreo : PlayingNotification() { @SuppressLint("RestrictedApi")
class PlayingNotificationOreo(
val context: Context
) : PlayingNotification(context) {
private var target: Target<BitmapPaletteWrapper>? = null private var primaryColor: Int = 0
private fun getCombinedRemoteViews(collapsed: Boolean, song: Song): RemoteViews { init {
val remoteViews = RemoteViews( val notificationLayout = getCombinedRemoteViews(true)
service.packageName, val notificationLayoutBig = getCombinedRemoteViews(false)
if (collapsed) R.layout.layout_notification_collapsed else R.layout.layout_notification_expanded
)
remoteViews.setTextViewText(
R.id.appName,
service.getString(R.string.app_name) + "" + song.albumName
)
remoteViews.setTextViewText(R.id.title, song.title)
remoteViews.setTextViewText(R.id.subtitle, song.artistName)
linkButtons(remoteViews)
return remoteViews
}
override fun update() { val action = Intent(context, MainActivity::class.java)
stopped = false
val song = service.currentSong
val isPlaying = service.isPlaying
val notificationLayout = getCombinedRemoteViews(true, song)
val notificationLayoutBig = getCombinedRemoteViews(false, song)
val action = Intent(service, MainActivity::class.java)
action.putExtra(MainActivity.EXPAND_PANEL, PreferenceUtil.isExpandPanel) action.putExtra(MainActivity.EXPAND_PANEL, PreferenceUtil.isExpandPanel)
action.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP action.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
val clickIntent = PendingIntent val clickIntent = PendingIntent
.getActivity( .getActivity(
service, context,
0, 0,
action, action,
PendingIntent.FLAG_UPDATE_CURRENT or if (VersionUtils.hasMarshmallow()) PendingIntent.FLAG_UPDATE_CURRENT or if (VersionUtils.hasMarshmallow())
PendingIntent.FLAG_IMMUTABLE PendingIntent.FLAG_IMMUTABLE
else 0 else 0
) )
val deleteIntent = buildPendingIntent(service, ACTION_QUIT, null) val deleteIntent = buildPendingIntent(context, ACTION_QUIT, null)
val builder = NotificationCompat.Builder(service, NOTIFICATION_CHANNEL_ID) setSmallIcon(R.drawable.ic_notification)
.setSmallIcon(R.drawable.ic_notification) setContentIntent(clickIntent)
.setContentIntent(clickIntent) setDeleteIntent(deleteIntent)
.setDeleteIntent(deleteIntent) setCategory(NotificationCompat.CATEGORY_SERVICE)
.setCategory(NotificationCompat.CATEGORY_SERVICE) priority = NotificationCompat.PRIORITY_MAX
.setPriority(NotificationCompat.PRIORITY_MAX) setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) setCustomContentView(notificationLayout)
.setCustomContentView(notificationLayout) setCustomBigContentView(notificationLayoutBig)
.setCustomBigContentView(notificationLayoutBig) setOngoing(true)
.setOngoing(isPlaying)
val bigNotificationImageSize = service.resources
.getDimensionPixelSize(R.dimen.notification_big_image_size)
service.runOnUiThread {
if (target != null) {
Glide.with(service).clear(target)
}
target = GlideApp.with(service).asBitmapPalette().songCoverOptions(song)
.load(RetroGlideExtension.getSongModel(song))
.centerCrop()
.into(object : SimpleTarget<BitmapPaletteWrapper>(
bigNotificationImageSize,
bigNotificationImageSize
) {
override fun onResourceReady(
resource: BitmapPaletteWrapper,
transition: Transition<in BitmapPaletteWrapper>?
) {
/* val mediaNotificationProcessor = MediaNotificationProcessor(
service,
service
) { i, _ -> update(resource.bitmap, i) }
mediaNotificationProcessor.processNotification(resource.bitmap)*/
val colors = MediaNotificationProcessor(service, resource.bitmap)
update(resource.bitmap, colors.backgroundColor)
}
override fun onLoadFailed(errorDrawable: Drawable?) {
super.onLoadFailed(errorDrawable)
update(
null,
resolveColor(service, R.attr.colorSurface, Color.WHITE)
)
}
private fun update(bitmap: Bitmap?, bgColor: Int) {
var bgColorFinal = bgColor
if (bitmap != null) {
notificationLayout.setImageViewBitmap(R.id.largeIcon, bitmap)
notificationLayoutBig.setImageViewBitmap(R.id.largeIcon, bitmap)
} else {
notificationLayout.setImageViewResource(
R.id.largeIcon,
R.drawable.default_audio_art
)
notificationLayoutBig.setImageViewResource(
R.id.largeIcon,
R.drawable.default_audio_art
)
}
// Android 12 applies a standard Notification template to every notification
// which will in turn have a default background so setting a different background
// than that, looks weird
if (!VersionUtils.hasS()) {
if (!PreferenceUtil.isColoredNotification) {
bgColorFinal =
resolveColor(service, R.attr.colorPrimary, Color.WHITE)
}
setBackgroundColor(bgColorFinal)
}
setNotificationContent(ColorUtil.isColorLight(bgColorFinal))
if (stopped) {
return // notification has been stopped before loading was finished
}
updateNotifyModeAndPostNotification(builder.build())
}
private fun setBackgroundColor(color: Int) {
notificationLayout.setInt(R.id.image, "setBackgroundColor", color)
notificationLayoutBig.setInt(R.id.image, "setBackgroundColor", color)
}
private fun setNotificationContent(dark: Boolean) {
val primary = MaterialValueHelper.getPrimaryTextColor(service, dark)
val secondary = MaterialValueHelper.getSecondaryTextColor(service, dark)
val close = createBitmap(
RetroUtil.getTintedVectorDrawable(
service,
R.drawable.ic_close,
primary
), NOTIFICATION_CONTROLS_SIZE_MULTIPLIER
)
val prev = createBitmap(
RetroUtil.getTintedVectorDrawable(
service,
R.drawable.ic_skip_previous_round_white_32dp,
primary
), NOTIFICATION_CONTROLS_SIZE_MULTIPLIER
)
val next = createBitmap(
RetroUtil.getTintedVectorDrawable(
service,
R.drawable.ic_skip_next_round_white_32dp,
primary
), NOTIFICATION_CONTROLS_SIZE_MULTIPLIER
)
val playPause = createBitmap(
RetroUtil.getTintedVectorDrawable(
service,
if (isPlaying)
R.drawable.ic_pause_white_48dp
else
R.drawable.ic_play_arrow_white_48dp, primary
), NOTIFICATION_CONTROLS_SIZE_MULTIPLIER
)
notificationLayout.setTextColor(R.id.title, primary)
notificationLayout.setTextColor(R.id.subtitle, secondary)
notificationLayout.setTextColor(R.id.appName, secondary)
notificationLayout.setImageViewBitmap(R.id.action_prev, prev)
notificationLayout.setImageViewBitmap(R.id.action_next, next)
notificationLayout.setImageViewBitmap(R.id.action_play_pause, playPause)
notificationLayoutBig.setTextColor(R.id.title, primary)
notificationLayoutBig.setTextColor(R.id.subtitle, secondary)
notificationLayoutBig.setTextColor(R.id.appName, secondary)
notificationLayoutBig.setImageViewBitmap(R.id.action_quit, close)
notificationLayoutBig.setImageViewBitmap(R.id.action_prev, prev)
notificationLayoutBig.setImageViewBitmap(R.id.action_next, next)
notificationLayoutBig.setImageViewBitmap(R.id.action_play_pause, playPause)
notificationLayout.setImageViewBitmap(
R.id.smallIcon,
createBitmap(
RetroUtil.getTintedVectorDrawable(
service,
R.drawable.ic_notification,
secondary
), 0.6f
)
)
notificationLayoutBig.setImageViewBitmap(
R.id.smallIcon,
createBitmap(
RetroUtil.getTintedVectorDrawable(
service,
R.drawable.ic_notification,
secondary
), 0.6f
)
)
}
})
}
if (stopped) {
return // notification has been stopped before loading was finished
}
updateNotifyModeAndPostNotification(builder.build())
} }
private fun getCombinedRemoteViews(collapsed: Boolean): RemoteViews {
val remoteViews = RemoteViews(
context.packageName,
if (collapsed) R.layout.layout_notification_collapsed else R.layout.layout_notification_expanded
)
linkButtons(remoteViews)
return remoteViews
}
@SuppressLint("RestrictedApi")
override fun updateMetadata(song: Song, onUpdate: () -> Unit) {
val bigNotificationImageSize = context.resources
.getDimensionPixelSize(R.dimen.notification_big_image_size)
GlideApp.with(context).asBitmapPalette().songCoverOptions(song)
.load(RetroGlideExtension.getSongModel(song))
.centerCrop()
.into(object : CustomTarget<BitmapPaletteWrapper>(
bigNotificationImageSize,
bigNotificationImageSize
) {
override fun onResourceReady(
resource: BitmapPaletteWrapper,
transition: Transition<in BitmapPaletteWrapper>?
) {
val colors = MediaNotificationProcessor(context, resource.bitmap)
update(resource.bitmap, colors.backgroundColor)
}
override fun onLoadFailed(errorDrawable: Drawable?) {
super.onLoadFailed(errorDrawable)
update(
null,
resolveColor(context, R.attr.colorSurface, Color.WHITE)
)
}
override fun onLoadCleared(placeholder: Drawable?) {
}
private fun update(bitmap: Bitmap?, bgColor: Int) {
var bgColorFinal = bgColor
if (bitmap != null) {
contentView.setImageViewBitmap(R.id.largeIcon, bitmap)
bigContentView.setImageViewBitmap(R.id.largeIcon, bitmap)
} else {
contentView.setImageViewResource(
R.id.largeIcon,
R.drawable.default_audio_art
)
bigContentView.setImageViewResource(
R.id.largeIcon,
R.drawable.default_audio_art
)
}
// Android 12 applies a standard Notification template to every notification
// which will in turn have a default background so setting a different background
// than that, looks weird
if (!VersionUtils.hasS()) {
if (!PreferenceUtil.isColoredNotification) {
bgColorFinal =
resolveColor(context, R.attr.colorSurface, Color.WHITE)
}
setBackgroundColor(bgColorFinal)
setNotificationContent(ColorUtil.isColorLight(bgColorFinal))
}else {
setNotificationContent(!ColorUtil.isColorLight(context.surfaceColor()))
}
onUpdate()
}
private fun setBackgroundColor(color: Int) {
contentView.setInt(R.id.image, "setBackgroundColor", color)
bigContentView.setInt(R.id.image, "setBackgroundColor", color)
}
private fun setNotificationContent(dark: Boolean) {
val primary = MaterialValueHelper.getPrimaryTextColor(context, dark)
val secondary = MaterialValueHelper.getSecondaryTextColor(context, dark)
primaryColor = primary
val close = createBitmap(
RetroUtil.getTintedVectorDrawable(
context,
R.drawable.ic_close,
primary
), NOTIFICATION_CONTROLS_SIZE_MULTIPLIER
)
val prev = createBitmap(
RetroUtil.getTintedVectorDrawable(
context,
R.drawable.ic_skip_previous_round_white_32dp,
primary
), NOTIFICATION_CONTROLS_SIZE_MULTIPLIER
)
val next = createBitmap(
RetroUtil.getTintedVectorDrawable(
context,
R.drawable.ic_skip_next_round_white_32dp,
primary
), NOTIFICATION_CONTROLS_SIZE_MULTIPLIER
)
val playPause = getPlayPauseBitmap(true)
contentView.setTextColor(R.id.title, primary)
contentView.setTextColor(R.id.subtitle, secondary)
contentView.setTextColor(R.id.appName, secondary)
contentView.setImageViewBitmap(R.id.action_prev, prev)
contentView.setImageViewBitmap(R.id.action_next, next)
contentView.setImageViewBitmap(R.id.action_play_pause, playPause)
contentView.setTextViewText(
R.id.appName,
context.getString(R.string.app_name) + "" + song.albumName
)
contentView.setTextViewText(R.id.title, song.title)
contentView.setTextViewText(R.id.subtitle, song.artistName)
bigContentView.setTextColor(R.id.title, primary)
bigContentView.setTextColor(R.id.subtitle, secondary)
bigContentView.setTextColor(R.id.appName, secondary)
bigContentView.setImageViewBitmap(R.id.action_quit, close)
bigContentView.setImageViewBitmap(R.id.action_prev, prev)
bigContentView.setImageViewBitmap(R.id.action_next, next)
bigContentView.setImageViewBitmap(R.id.action_play_pause, playPause)
bigContentView.setTextViewText(
R.id.appName,
context.getString(R.string.app_name) + "" + song.albumName
)
bigContentView.setTextViewText(R.id.title, song.title)
bigContentView.setTextViewText(R.id.subtitle, song.artistName)
contentView.setImageViewBitmap(
R.id.smallIcon,
createBitmap(
RetroUtil.getTintedVectorDrawable(
context,
R.drawable.ic_notification,
secondary
), 0.6f
)
)
bigContentView.setImageViewBitmap(
R.id.smallIcon,
createBitmap(
RetroUtil.getTintedVectorDrawable(
context,
R.drawable.ic_notification,
secondary
), 0.6f
)
)
}
})
}
private fun getPlayPauseBitmap(isPlaying: Boolean): Bitmap {
return createBitmap(
RetroUtil.getTintedVectorDrawable(
context,
if (isPlaying)
R.drawable.ic_pause_white_48dp
else
R.drawable.ic_play_arrow_white_48dp, primaryColor
), NOTIFICATION_CONTROLS_SIZE_MULTIPLIER)
}
override fun setPlaying(isPlaying: Boolean) {
getPlayPauseBitmap(isPlaying).also {
contentView.setImageViewBitmap(R.id.action_play_pause, it)
bigContentView.setImageViewBitmap(R.id.action_play_pause, it)
}
}
override fun updateFavorite(song: Song, onUpdate: () -> Unit) {
}
private fun buildPendingIntent( private fun buildPendingIntent(
context: Context, action: String, context: Context, action: String,
@ -275,23 +286,34 @@ class PlayingNotificationOreo : PlayingNotification() {
private fun linkButtons(notificationLayout: RemoteViews) { private fun linkButtons(notificationLayout: RemoteViews) {
var pendingIntent: PendingIntent var pendingIntent: PendingIntent
val serviceName = ComponentName(service, MusicService::class.java) val serviceName = ComponentName(context, MusicService::class.java)
// Previous track // Previous track
pendingIntent = buildPendingIntent(service, ACTION_REWIND, serviceName) pendingIntent = buildPendingIntent(context, ACTION_REWIND, serviceName)
notificationLayout.setOnClickPendingIntent(R.id.action_prev, pendingIntent) notificationLayout.setOnClickPendingIntent(R.id.action_prev, pendingIntent)
// Play and pause // Play and pause
pendingIntent = buildPendingIntent(service, ACTION_TOGGLE_PAUSE, serviceName) pendingIntent = buildPendingIntent(context, ACTION_TOGGLE_PAUSE, serviceName)
notificationLayout.setOnClickPendingIntent(R.id.action_play_pause, pendingIntent) notificationLayout.setOnClickPendingIntent(R.id.action_play_pause, pendingIntent)
// Next track // Next track
pendingIntent = buildPendingIntent(service, ACTION_SKIP, serviceName) pendingIntent = buildPendingIntent(context, ACTION_SKIP, serviceName)
notificationLayout.setOnClickPendingIntent(R.id.action_next, pendingIntent) notificationLayout.setOnClickPendingIntent(R.id.action_next, pendingIntent)
// Close // Close
pendingIntent = buildPendingIntent(service, ACTION_QUIT, serviceName) pendingIntent = buildPendingIntent(context, ACTION_QUIT, serviceName)
notificationLayout.setOnClickPendingIntent(R.id.action_quit, pendingIntent) notificationLayout.setOnClickPendingIntent(R.id.action_quit, pendingIntent)
} }
companion object {
fun from(
context: Context,
notificationManager: NotificationManager
): PlayingNotification {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createNotificationChannel(context, notificationManager)
}
return PlayingNotificationOreo(context)
}
}
} }