From 1df50491fcb1223684e49bc5fcd24db116dc896e Mon Sep 17 00:00:00 2001 From: Prathamesh More Date: Sun, 5 Dec 2021 14:52:03 +0530 Subject: [PATCH] [Notification] Fixed and improved playing notification --- .../retromusic/service/MusicService.java | 71 ++- .../notification/PlayingNotification.kt | 110 ++--- .../notification/PlayingNotificationImpl.kt | 320 ++++++------- .../notification/PlayingNotificationOreo.kt | 424 +++++++++--------- 4 files changed, 482 insertions(+), 443 deletions(-) diff --git a/app/src/main/java/code/name/monkey/retromusic/service/MusicService.java b/app/src/main/java/code/name/monkey/retromusic/service/MusicService.java index 99067cd4..d9946b82 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/MusicService.java +++ b/app/src/main/java/code/name/monkey/retromusic/service/MusicService.java @@ -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.service.AudioFader.startFadeAnimator; +import android.app.NotificationManager; import android.app.PendingIntent; import android.appwidget.AppWidgetManager; import android.bluetooth.BluetoothDevice; @@ -34,6 +35,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; +import android.content.pm.ServiceInfo; import android.database.ContentObserver; import android.graphics.Bitmap; 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.volume.AudioVolumeObserver; import code.name.monkey.retromusic.volume.OnAudioVolumeChangedListener; +import kotlin.Unit; /** * @author Karim Abou Zeid (kabouzeid), Andrew Neal @@ -280,7 +283,8 @@ public class MusicService extends MediaBrowserServiceCompat new BroadcastReceiver() { @Override public void onReceive(final Context context, final Intent intent) { - updateNotification(); + playingNotification.updateFavorite(getCurrentSong(), MusicService.this::startForegroundOrNotify); + startForegroundOrNotify(); } }; private final BroadcastReceiver lockScreenReceiver = @@ -363,6 +367,8 @@ public class MusicService extends MediaBrowserServiceCompat private ThrottledSeekHandler throttledSeekHandler; private Handler uiThreadHandler; private PowerManager.WakeLock wakeLock; + private NotificationManager notificationManager; + private boolean isForeground = false; private static Bitmap copy(Bitmap bitmap) { Bitmap.Config config = bitmap.getConfig(); @@ -424,6 +430,10 @@ public class MusicService extends MediaBrowserServiceCompat registerReceiver(updateFavoriteReceiver, new IntentFilter(FAVORITE_STATE_CHANGED)); registerReceiver(lockScreenReceiver, new IntentFilter(Intent.ACTION_SCREEN_OFF)); + setSessionToken(mediaSession.getSessionToken()); + if (VersionUtils.INSTANCE.hasMarshmallow()) { + notificationManager = getSystemService(NotificationManager.class); + } initNotification(); mediaStoreObserver = new MediaStoreObserver(this, playerHandler); @@ -474,7 +484,6 @@ public class MusicService extends MediaBrowserServiceCompat mPackageValidator = new PackageValidator(this, R.xml.allowed_media_browser_callers); mMusicProvider.setMusicService(this); - setSessionToken(mediaSession.getSessionToken()); } @Override @@ -772,11 +781,10 @@ public class MusicService extends MediaBrowserServiceCompat public void initNotification() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && !PreferenceUtil.INSTANCE.isClassicNotification()) { - playingNotification = new PlayingNotificationImpl(); + playingNotification = PlayingNotificationImpl.Companion.from(this, notificationManager, mediaSession); } else { - playingNotification = new PlayingNotificationOreo(); + playingNotification = PlayingNotificationOreo.Companion.from(this, notificationManager); } - playingNotification.init(this); } public boolean isLastTrack() { @@ -844,7 +852,8 @@ public class MusicService extends MediaBrowserServiceCompat // Request from an untrusted package: return an empty browser root return new BrowserRoot(AutoMediaIDHelper.MEDIA_ID_EMPTY_ROOT, null); } 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. */ boolean isRecentRequest = false; @@ -889,7 +898,7 @@ public class MusicService extends MediaBrowserServiceCompat /* Switch to MultiPlayer if Crossfade duration is 0 and Playback is not an instance of MultiPlayer */ if (playback != null) - playback.setCrossFadeDuration(PreferenceUtil.INSTANCE.getCrossFadeDuration()); + playback.setCrossFadeDuration(PreferenceUtil.INSTANCE.getCrossFadeDuration()); if (!(playback instanceof MultiPlayer) && PreferenceUtil.INSTANCE.getCrossFadeDuration() == 0) { if (playback != null) { playback.release(); @@ -1049,6 +1058,7 @@ public class MusicService extends MediaBrowserServiceCompat } public void pause() { + Log.i(TAG, "Paused"); pausedByTransientLossOfFocus = false; if (playback != null && playback.isPlaying()) { startFadeAnimator(playback, false, () -> { @@ -1170,7 +1180,8 @@ public class MusicService extends MediaBrowserServiceCompat public void quit() { pause(); - playingNotification.stop(); + stopForeground(true); + notificationManager.cancel(PlayingNotification.NOTIFICATION_ID); closeAudioEffectSession(); getAudioManager().abandonAudioFocus(audioFocusListener); @@ -1333,7 +1344,8 @@ public class MusicService extends MediaBrowserServiceCompat public void updateNotification() { 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) { switch (what) { case PLAY_STATE_CHANGED: - updateNotification(); updateMediaSessionPlaybackState(); final boolean isPlaying = isPlaying(); if (!isPlaying && getSongProgressMillis() > 0) { savePositionInTrack(); } songPlayCountHelper.notifyPlayStateChanged(isPlaying); + playingNotification.setPlaying(isPlaying); + startForegroundOrNotify(); break; case FAVORITE_STATE_CHANGED: + playingNotification.updateFavorite(getCurrentSong(), this::startForegroundOrNotify); case META_CHANGED: - updateNotification(); + playingNotification.updateMetadata(getCurrentSong(), this::startForegroundOrNotify); updateMediaSessionMetaData(); updateMediaSessionPlaybackState(); savePosition(); @@ -1435,12 +1449,41 @@ public class MusicService extends MediaBrowserServiceCompat if (playingQueue.size() > 0) { prepareNext(); } else { - playingNotification.stop(); + stopForegroundAndNotification(); } 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() { synchronized (this) { try { @@ -1600,8 +1643,8 @@ public class MusicService extends MediaBrowserServiceCompat mediaButtonIntent.setComponent(mediaButtonReceiverComponentName); PendingIntent mediaButtonReceiverPendingIntent; - mediaButtonReceiverPendingIntent = PendingIntent.getBroadcast(getApplicationContext(), 0, mediaButtonIntent, - VersionUtils.INSTANCE.hasMarshmallow() ? PendingIntent.FLAG_IMMUTABLE : 0); + mediaButtonReceiverPendingIntent = PendingIntent.getBroadcast(getApplicationContext(), 0, mediaButtonIntent, + VersionUtils.INSTANCE.hasMarshmallow() ? PendingIntent.FLAG_IMMUTABLE : 0); mediaSession = new MediaSessionCompat( this, diff --git a/app/src/main/java/code/name/monkey/retromusic/service/notification/PlayingNotification.kt b/app/src/main/java/code/name/monkey/retromusic/service/notification/PlayingNotification.kt index 5557a285..162775f0 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/notification/PlayingNotification.kt +++ b/app/src/main/java/code/name/monkey/retromusic/service/notification/PlayingNotification.kt @@ -15,95 +15,51 @@ package code.name.monkey.retromusic.service.notification -import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager -import android.content.Context.NOTIFICATION_SERVICE -import android.content.pm.ServiceInfo -import android.os.Build +import android.content.Context import androidx.annotation.RequiresApi +import androidx.core.app.NotificationCompat import code.name.monkey.retromusic.R -import code.name.monkey.retromusic.service.MusicService +import code.name.monkey.retromusic.model.Song -abstract class PlayingNotification { - protected lateinit var service: MusicService - protected var stopped: Boolean = false - private var notifyMode = NOTIFY_MODE_BACKGROUND - private var notificationManager: NotificationManager? = null +abstract class PlayingNotification(context: Context) : + NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID) { + abstract fun updateMetadata(song: Song, onUpdate: () -> Unit) - @Synchronized - 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 setPlaying(isPlaying: Boolean) - abstract fun update() - - @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) - } - } + abstract fun updateFavorite(song: Song, onUpdate: () -> Unit) companion object { const val NOTIFICATION_CONTROLS_SIZE_MULTIPLIER = 1.0f internal const val NOTIFICATION_CHANNEL_ID = "playing_notification" - private const val NOTIFICATION_ID = 1 - private const val NOTIFY_MODE_FOREGROUND = 1 - private const val NOTIFY_MODE_BACKGROUND = 0 + const val NOTIFICATION_ID = 1 + + + @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) + } + } } } \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/service/notification/PlayingNotificationImpl.kt b/app/src/main/java/code/name/monkey/retromusic/service/notification/PlayingNotificationImpl.kt index f7f58667..49d43a87 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/notification/PlayingNotificationImpl.kt +++ b/app/src/main/java/code/name/monkey/retromusic/service/notification/PlayingNotificationImpl.kt @@ -14,14 +14,16 @@ package code.name.monkey.retromusic.service.notification +import android.annotation.SuppressLint +import android.app.NotificationManager import android.app.PendingIntent import android.content.ComponentName +import android.content.Context import android.content.Intent -import android.graphics.Bitmap -import android.graphics.BitmapFactory import android.graphics.Color import android.graphics.drawable.Drawable import android.os.Build +import android.support.v4.media.session.MediaSessionCompat import androidx.core.app.NotificationCompat import androidx.core.text.HtmlCompat 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.RetroGlideExtension 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.util.MusicUtil import code.name.monkey.retromusic.util.PreferenceUtil import code.name.monkey.retromusic.util.RetroColorUtil -import com.bumptech.glide.Glide -import com.bumptech.glide.request.target.SimpleTarget -import com.bumptech.glide.request.target.Target +import com.bumptech.glide.request.target.CustomTarget import com.bumptech.glide.request.transition.Transition +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch -import org.koin.core.component.KoinComponent +import kotlinx.coroutines.withContext -class PlayingNotificationImpl : PlayingNotification(), KoinComponent { - private var target: Target? = null +@SuppressLint("RestrictedApi") +class PlayingNotificationImpl( + val context: Context, + mediaSessionToken: MediaSessionCompat.Token +) : PlayingNotification(context) { - @Synchronized - override fun update() { - stopped = false - GlobalScope.launch { - val song = service.currentSong + init { + val action = Intent(context, 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( + 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( + "" + song.albumName + "", + 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( + bigNotificationImageSize, + bigNotificationImageSize + ) { + override fun onResourceReady( + resource: BitmapPaletteWrapper, + transition: Transition? + ) { + 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 isPlaying = service.isPlaying val isFavorite = if (playlist != null) { val songEntity = song.toSongEntity(playlist.playListId) MusicUtil.repository.isFavoriteSong(songEntity).isNotEmpty() } else false - - val playButtonResId = - if (isPlaying) R.drawable.ic_pause_white_48dp else R.drawable.ic_play_arrow_white_48dp - 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( - bigNotificationImageSize, - bigNotificationImageSize - ) { - override fun onResourceReady( - resource: BitmapPaletteWrapper, - transition: Transition? - ) { - 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( - "" + song.title + "", - HtmlCompat.FROM_HTML_MODE_LEGACY - ) - ) - .setContentText(song.artistName) - .setSubText( - HtmlCompat.fromHtml( - "" + song.albumName + "", - 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()) - } - }) + withContext(Dispatchers.Main) { + mActions[0] = buildFavoriteAction(isFavorite) + onUpdate() } } } private fun retrievePlaybackAction(action: String): PendingIntent { - val serviceName = ComponentName(service, MusicService::class.java) + val serviceName = ComponentName(context, MusicService::class.java) val intent = Intent(action) intent.component = serviceName 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 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) + } + } } \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/service/notification/PlayingNotificationOreo.kt b/app/src/main/java/code/name/monkey/retromusic/service/notification/PlayingNotificationOreo.kt index 29ff5287..a314ca2e 100644 --- a/app/src/main/java/code/name/monkey/retromusic/service/notification/PlayingNotificationOreo.kt +++ b/app/src/main/java/code/name/monkey/retromusic/service/notification/PlayingNotificationOreo.kt @@ -14,6 +14,8 @@ package code.name.monkey.retromusic.service.notification +import android.annotation.SuppressLint +import android.app.NotificationManager import android.app.PendingIntent import android.content.ComponentName import android.content.Context @@ -21,6 +23,7 @@ import android.content.Intent import android.graphics.Bitmap import android.graphics.Color import android.graphics.drawable.Drawable +import android.os.Build import android.widget.RemoteViews import androidx.core.app.NotificationCompat 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.retromusic.R 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.RetroGlideExtension 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.createBitmap import code.name.monkey.retromusic.util.color.MediaNotificationProcessor -import com.bumptech.glide.Glide -import com.bumptech.glide.request.target.SimpleTarget -import com.bumptech.glide.request.target.Target +import com.bumptech.glide.request.target.CustomTarget import com.bumptech.glide.request.transition.Transition /** * @author Hemanth S (h4h13). */ -class PlayingNotificationOreo : PlayingNotification() { +@SuppressLint("RestrictedApi") +class PlayingNotificationOreo( + val context: Context +) : PlayingNotification(context) { - private var target: Target? = null + private var primaryColor: Int = 0 - private fun getCombinedRemoteViews(collapsed: Boolean, song: Song): RemoteViews { - val remoteViews = RemoteViews( - service.packageName, - 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 - } + init { + val notificationLayout = getCombinedRemoteViews(true) + val notificationLayoutBig = getCombinedRemoteViews(false) - override fun update() { - 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) + val action = Intent(context, 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, + context, 0, action, PendingIntent.FLAG_UPDATE_CURRENT or if (VersionUtils.hasMarshmallow()) PendingIntent.FLAG_IMMUTABLE 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) - .setContentIntent(clickIntent) - .setDeleteIntent(deleteIntent) - .setCategory(NotificationCompat.CATEGORY_SERVICE) - .setPriority(NotificationCompat.PRIORITY_MAX) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - .setCustomContentView(notificationLayout) - .setCustomBigContentView(notificationLayoutBig) - .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( - bigNotificationImageSize, - bigNotificationImageSize - ) { - override fun onResourceReady( - resource: BitmapPaletteWrapper, - transition: Transition? - ) { - /* 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()) + setSmallIcon(R.drawable.ic_notification) + setContentIntent(clickIntent) + setDeleteIntent(deleteIntent) + setCategory(NotificationCompat.CATEGORY_SERVICE) + priority = NotificationCompat.PRIORITY_MAX + setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + setCustomContentView(notificationLayout) + setCustomBigContentView(notificationLayoutBig) + setOngoing(true) } + 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( + bigNotificationImageSize, + bigNotificationImageSize + ) { + override fun onResourceReady( + resource: BitmapPaletteWrapper, + transition: Transition? + ) { + 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( context: Context, action: String, @@ -275,23 +286,34 @@ class PlayingNotificationOreo : PlayingNotification() { private fun linkButtons(notificationLayout: RemoteViews) { var pendingIntent: PendingIntent - val serviceName = ComponentName(service, MusicService::class.java) + val serviceName = ComponentName(context, MusicService::class.java) // Previous track - pendingIntent = buildPendingIntent(service, ACTION_REWIND, serviceName) + pendingIntent = buildPendingIntent(context, ACTION_REWIND, serviceName) notificationLayout.setOnClickPendingIntent(R.id.action_prev, pendingIntent) // 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) // Next track - pendingIntent = buildPendingIntent(service, ACTION_SKIP, serviceName) + pendingIntent = buildPendingIntent(context, ACTION_SKIP, serviceName) notificationLayout.setOnClickPendingIntent(R.id.action_next, pendingIntent) // Close - pendingIntent = buildPendingIntent(service, ACTION_QUIT, serviceName) + pendingIntent = buildPendingIntent(context, ACTION_QUIT, serviceName) 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) + } + } }