Unify notification
Unify the notification extensions into a single PlaybackNotification object that does not rely on PlaybackStateManager and SettingsManager.
This commit is contained in:
parent
ca4cabedbc
commit
7524589969
11 changed files with 295 additions and 280 deletions
|
@ -42,7 +42,7 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<service
|
<service
|
||||||
android:name=".playback.PlaybackService"
|
android:name=".playback.system.PlaybackService"
|
||||||
android:description="@string/info_service_desc"
|
android:description="@string/info_service_desc"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
|
|
|
@ -9,7 +9,7 @@ import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.databinding.DataBindingUtil
|
import androidx.databinding.DataBindingUtil
|
||||||
import org.oxycblt.auxio.databinding.ActivityMainBinding
|
import org.oxycblt.auxio.databinding.ActivityMainBinding
|
||||||
import org.oxycblt.auxio.playback.PlaybackService
|
import org.oxycblt.auxio.playback.system.PlaybackService
|
||||||
import org.oxycblt.auxio.settings.SettingsManager
|
import org.oxycblt.auxio.settings.SettingsManager
|
||||||
import org.oxycblt.auxio.ui.Accent
|
import org.oxycblt.auxio.ui.Accent
|
||||||
import org.oxycblt.auxio.ui.isEdgeOn
|
import org.oxycblt.auxio.ui.isEdgeOn
|
||||||
|
@ -49,7 +49,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
override fun onNewIntent(intent: Intent?) {
|
override fun onNewIntent(intent: Intent?) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
|
|
||||||
// Since the activity is set to singleTask [Given that theres only MainActivity]
|
// Since the activity is set to singleInstance [Given that there's only MainActivity]
|
||||||
// We have to manually push the intent whenever we get one so that MainFragment
|
// We have to manually push the intent whenever we get one so that MainFragment
|
||||||
// can catch any file intents
|
// can catch any file intents
|
||||||
setIntent(intent)
|
setIntent(intent)
|
||||||
|
|
|
@ -122,6 +122,8 @@ class MainFragment : Fragment() {
|
||||||
val activity = requireActivity()
|
val activity = requireActivity()
|
||||||
val intent = activity.intent
|
val intent = activity.intent
|
||||||
|
|
||||||
|
// If the intent of the activity is a file intent, then play it.
|
||||||
|
// TODO?: Add an option to view it instead of play it if this becomes too annoying
|
||||||
if (intent != null && intent.action == Intent.ACTION_VIEW) {
|
if (intent != null && intent.action == Intent.ACTION_VIEW) {
|
||||||
playbackModel.playWithIntent(intent, requireContext())
|
playbackModel.playWithIntent(intent, requireContext())
|
||||||
|
|
||||||
|
|
|
@ -1,221 +0,0 @@
|
||||||
package org.oxycblt.auxio.playback
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.NotificationChannel
|
|
||||||
import android.app.NotificationManager
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Build
|
|
||||||
import android.support.v4.media.session.MediaSessionCompat
|
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import androidx.media.app.NotificationCompat.MediaStyle
|
|
||||||
import org.oxycblt.auxio.BuildConfig
|
|
||||||
import org.oxycblt.auxio.MainActivity
|
|
||||||
import org.oxycblt.auxio.R
|
|
||||||
import org.oxycblt.auxio.coil.loadBitmap
|
|
||||||
import org.oxycblt.auxio.logE
|
|
||||||
import org.oxycblt.auxio.music.Genre
|
|
||||||
import org.oxycblt.auxio.music.Song
|
|
||||||
import org.oxycblt.auxio.playback.state.LoopMode
|
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackMode
|
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
|
||||||
|
|
||||||
object NotificationUtils {
|
|
||||||
const val CHANNEL_ID = "CHANNEL_AUXIO_PLAYBACK"
|
|
||||||
const val NOTIFICATION_ID = 0xA0A0
|
|
||||||
const val REQUEST_CODE = 0xA0C0
|
|
||||||
|
|
||||||
// The build type is applied to each action so that broadcasts will not conflict with debug/release builds.
|
|
||||||
const val ACTION_LOOP = "ACTION_AUXIO_LOOP_" + BuildConfig.BUILD_TYPE
|
|
||||||
const val ACTION_SHUFFLE = "ACTION_AUXIO_SHUFFLE_" + BuildConfig.BUILD_TYPE
|
|
||||||
const val ACTION_SKIP_PREV = "ACTION_AUXIO_SKIP_PREV_" + BuildConfig.BUILD_TYPE
|
|
||||||
const val ACTION_PLAY_PAUSE = "ACTION_AUXIO_PLAY_PAUSE_" + BuildConfig.BUILD_TYPE
|
|
||||||
const val ACTION_SKIP_NEXT = "ACTION_AUXIO_SKIP_NEXT_" + BuildConfig.BUILD_TYPE
|
|
||||||
const val ACTION_EXIT = "ACTION_AUXIO_EXIT_" + BuildConfig.BUILD_TYPE
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create the standard media notification used by Auxio.
|
|
||||||
* @param context [Context] required to create the notification
|
|
||||||
* @param mediaSession [MediaSessionCompat] required for the [MediaStyle] notification
|
|
||||||
* @author OxygenCobalt
|
|
||||||
*/
|
|
||||||
fun NotificationManager.createMediaNotification(
|
|
||||||
context: Context,
|
|
||||||
mediaSession: MediaSessionCompat
|
|
||||||
): NotificationCompat.Builder {
|
|
||||||
// Create a notification channel if required
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
val channel = NotificationChannel(
|
|
||||||
NotificationUtils.CHANNEL_ID,
|
|
||||||
context.getString(R.string.info_channel_name),
|
|
||||||
NotificationManager.IMPORTANCE_DEFAULT
|
|
||||||
)
|
|
||||||
|
|
||||||
createNotificationChannel(channel)
|
|
||||||
}
|
|
||||||
|
|
||||||
val mainIntent = PendingIntent.getActivity(
|
|
||||||
context, NotificationUtils.REQUEST_CODE,
|
|
||||||
Intent(context, MainActivity::class.java),
|
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT
|
|
||||||
)
|
|
||||||
|
|
||||||
return NotificationCompat.Builder(context, NotificationUtils.CHANNEL_ID)
|
|
||||||
.setSmallIcon(R.drawable.ic_song)
|
|
||||||
.setStyle(
|
|
||||||
MediaStyle()
|
|
||||||
.setMediaSession(mediaSession.sessionToken)
|
|
||||||
.setShowActionsInCompactView(1, 2, 3)
|
|
||||||
)
|
|
||||||
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
|
||||||
.setChannelId(NotificationUtils.CHANNEL_ID)
|
|
||||||
.setShowWhen(false)
|
|
||||||
.addAction(newAction(NotificationUtils.ACTION_LOOP, context))
|
|
||||||
.addAction(newAction(NotificationUtils.ACTION_SKIP_PREV, context))
|
|
||||||
.addAction(newAction(NotificationUtils.ACTION_PLAY_PAUSE, context))
|
|
||||||
.addAction(newAction(NotificationUtils.ACTION_SKIP_NEXT, context))
|
|
||||||
.addAction(newAction(NotificationUtils.ACTION_EXIT, context))
|
|
||||||
.setNotificationSilent()
|
|
||||||
.setContentIntent(mainIntent)
|
|
||||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the current metadata of a media notification.
|
|
||||||
* @param context The [Context] needed to load the cover bitmap
|
|
||||||
* @param song The [Song] that the notification should reflect
|
|
||||||
* @param colorize Whether to load the album art and colorize the notification based off it
|
|
||||||
* @param onDone A callback for when the process is finished
|
|
||||||
* @author OxygenCobalt
|
|
||||||
*/
|
|
||||||
fun NotificationCompat.Builder.setMetadata(
|
|
||||||
context: Context,
|
|
||||||
song: Song,
|
|
||||||
colorize: Boolean,
|
|
||||||
onDone: () -> Unit
|
|
||||||
) {
|
|
||||||
setContentTitle(song.name)
|
|
||||||
setContentText(
|
|
||||||
song.album.artist.name,
|
|
||||||
)
|
|
||||||
|
|
||||||
// On older versions of android [API <24], show the song's album on the subtext instead of
|
|
||||||
// the current mode, as that makes more sense for the old style of media notifications.
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
|
||||||
setSubText(song.album.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (colorize) {
|
|
||||||
// getBitmap() is concurrent, so only call back to the object calling this function when
|
|
||||||
// the loading is over.
|
|
||||||
loadBitmap(context, song) {
|
|
||||||
setLargeIcon(it)
|
|
||||||
|
|
||||||
onDone()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setLargeIcon(null)
|
|
||||||
|
|
||||||
onDone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the playing button on the media notification.
|
|
||||||
* @param context The context required to refresh the action
|
|
||||||
*/
|
|
||||||
@SuppressLint("RestrictedApi")
|
|
||||||
fun NotificationCompat.Builder.updatePlaying(context: Context) {
|
|
||||||
mActions[2] = newAction(NotificationUtils.ACTION_PLAY_PAUSE, context)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the extra action on the media notification [E.G the Loop/Shuffle button]
|
|
||||||
* @param context The context required to refresh the action
|
|
||||||
* @param useAltAction Whether to use the shuffle action or not, true if yes, false if no
|
|
||||||
*/
|
|
||||||
@SuppressLint("RestrictedApi")
|
|
||||||
fun NotificationCompat.Builder.updateExtraAction(context: Context, useAltAction: Boolean) {
|
|
||||||
mActions[0] = if (useAltAction) {
|
|
||||||
newAction(NotificationUtils.ACTION_SHUFFLE, context)
|
|
||||||
} else {
|
|
||||||
newAction(NotificationUtils.ACTION_LOOP, context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the subtext of the media notification to reflect the current mode.
|
|
||||||
* @param context The context required to get the strings required to show certain modes
|
|
||||||
*/
|
|
||||||
fun NotificationCompat.Builder.updateMode(context: Context) {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
||||||
val playbackManager = PlaybackStateManager.getInstance()
|
|
||||||
|
|
||||||
// If playing from all songs, set the subtext as that, otherwise the currently played parent.
|
|
||||||
if (playbackManager.mode == PlaybackMode.ALL_SONGS) {
|
|
||||||
setSubText(context.getString(R.string.label_all_songs))
|
|
||||||
} else {
|
|
||||||
val parent = playbackManager.parent
|
|
||||||
|
|
||||||
if (parent != null) {
|
|
||||||
setSubText(if (parent is Genre) parent.displayName else parent.name)
|
|
||||||
} else {
|
|
||||||
logE("Parent was null when it shouldnt have been.")
|
|
||||||
setSubText("")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new [NotificationCompat.Action].
|
|
||||||
* @param action The action that the notification action should represent
|
|
||||||
* @param context The [Context] needed to create the action
|
|
||||||
*/
|
|
||||||
private fun newAction(action: String, context: Context): NotificationCompat.Action {
|
|
||||||
val playbackManager = PlaybackStateManager.getInstance()
|
|
||||||
|
|
||||||
// Get the icon depending on the action & current state.
|
|
||||||
val drawable = when (action) {
|
|
||||||
NotificationUtils.ACTION_LOOP -> {
|
|
||||||
when (playbackManager.loopMode) {
|
|
||||||
LoopMode.NONE -> R.drawable.ic_loop_inactive
|
|
||||||
LoopMode.ONCE -> R.drawable.ic_loop_one
|
|
||||||
LoopMode.INFINITE -> R.drawable.ic_loop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NotificationUtils.ACTION_SHUFFLE -> {
|
|
||||||
if (playbackManager.isShuffling) {
|
|
||||||
R.drawable.ic_shuffle
|
|
||||||
} else {
|
|
||||||
R.drawable.ic_shuffle_inactive
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NotificationUtils.ACTION_SKIP_PREV -> R.drawable.ic_skip_prev
|
|
||||||
|
|
||||||
NotificationUtils.ACTION_PLAY_PAUSE -> {
|
|
||||||
if (playbackManager.isPlaying) {
|
|
||||||
R.drawable.ic_pause
|
|
||||||
} else {
|
|
||||||
R.drawable.ic_play
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NotificationUtils.ACTION_SKIP_NEXT -> R.drawable.ic_skip_next
|
|
||||||
|
|
||||||
NotificationUtils.ACTION_EXIT -> R.drawable.ic_exit
|
|
||||||
else -> R.drawable.ic_error
|
|
||||||
}
|
|
||||||
|
|
||||||
return NotificationCompat.Action.Builder(
|
|
||||||
drawable, action,
|
|
||||||
PendingIntent.getBroadcast(
|
|
||||||
context, NotificationUtils.REQUEST_CODE,
|
|
||||||
Intent(action), PendingIntent.FLAG_UPDATE_CURRENT
|
|
||||||
)
|
|
||||||
).build()
|
|
||||||
}
|
|
|
@ -114,7 +114,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
/**
|
/**
|
||||||
* Play a song.
|
* Play a song.
|
||||||
* @param song The song to be played
|
* @param song The song to be played
|
||||||
* @param mode The [PlaybackMode] for it to be played in. Defaults to the preferred song playback mode if not specified.
|
* @param mode The [PlaybackMode] for it to be played in. Defaults to the preferred song playback mode of the user if not specified.
|
||||||
*/
|
*/
|
||||||
fun playSong(song: Song, mode: PlaybackMode = settingsManager.songPlaybackMode) {
|
fun playSong(song: Song, mode: PlaybackMode = settingsManager.songPlaybackMode) {
|
||||||
playbackManager.playSong(song, mode)
|
playbackManager.playSong(song, mode)
|
||||||
|
@ -413,8 +413,8 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
mIsShuffling.value = isShuffling
|
mIsShuffling.value = isShuffling
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLoopUpdate(mode: LoopMode) {
|
override fun onLoopUpdate(loopMode: LoopMode) {
|
||||||
mLoopMode.value = mode
|
mLoopMode.value = loopMode
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onInUserQueueUpdate(isInUserQueue: Boolean) {
|
override fun onInUserQueueUpdate(isInUserQueue: Boolean) {
|
||||||
|
|
|
@ -22,7 +22,7 @@ import org.oxycblt.auxio.settings.SettingsManager
|
||||||
*
|
*
|
||||||
* This should ***NOT*** be used outside of the playback module.
|
* This should ***NOT*** be used outside of the playback module.
|
||||||
* - If you want to use the playback state in the UI, use [org.oxycblt.auxio.playback.PlaybackViewModel] as it can withstand volatile UIs.
|
* - If you want to use the playback state in the UI, use [org.oxycblt.auxio.playback.PlaybackViewModel] as it can withstand volatile UIs.
|
||||||
* - If you want to use the playback state with the ExoPlayer instance or system-side things, use [org.oxycblt.auxio.playback.PlaybackService].
|
* - If you want to use the playback state with the ExoPlayer instance or system-side things, use [org.oxycblt.auxio.playback.system.PlaybackService].
|
||||||
*
|
*
|
||||||
* All access should be done with [PlaybackStateManager.getInstance].
|
* All access should be done with [PlaybackStateManager.getInstance].
|
||||||
*
|
*
|
||||||
|
@ -454,6 +454,7 @@ class PlaybackStateManager private constructor() {
|
||||||
fun shuffleAll() {
|
fun shuffleAll() {
|
||||||
mMode = PlaybackMode.ALL_SONGS
|
mMode = PlaybackMode.ALL_SONGS
|
||||||
mQueue = musicStore.songs.toMutableList()
|
mQueue = musicStore.songs.toMutableList()
|
||||||
|
mParent = null
|
||||||
|
|
||||||
setShuffling(true, keepSong = false)
|
setShuffling(true, keepSong = false)
|
||||||
updatePlayback(mQueue[0])
|
updatePlayback(mQueue[0])
|
||||||
|
@ -809,7 +810,7 @@ class PlaybackStateManager private constructor() {
|
||||||
fun onIndexUpdate(index: Int) {}
|
fun onIndexUpdate(index: Int) {}
|
||||||
fun onPlayingUpdate(isPlaying: Boolean) {}
|
fun onPlayingUpdate(isPlaying: Boolean) {}
|
||||||
fun onShuffleUpdate(isShuffling: Boolean) {}
|
fun onShuffleUpdate(isShuffling: Boolean) {}
|
||||||
fun onLoopUpdate(mode: LoopMode) {}
|
fun onLoopUpdate(loopMode: LoopMode) {}
|
||||||
fun onSeek(position: Long) {}
|
fun onSeek(position: Long) {}
|
||||||
fun onInUserQueueUpdate(isInUserQueue: Boolean) {}
|
fun onInUserQueueUpdate(isInUserQueue: Boolean) {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package org.oxycblt.auxio.playback
|
package org.oxycblt.auxio.playback.system
|
||||||
|
|
||||||
import android.animation.ValueAnimator
|
import android.animation.ValueAnimator
|
||||||
import android.content.Context
|
import android.content.Context
|
|
@ -0,0 +1,219 @@
|
||||||
|
package org.oxycblt.auxio.playback.system
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.support.v4.media.session.MediaSessionCompat
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import org.oxycblt.auxio.BuildConfig
|
||||||
|
import org.oxycblt.auxio.MainActivity
|
||||||
|
import org.oxycblt.auxio.R
|
||||||
|
import org.oxycblt.auxio.coil.loadBitmap
|
||||||
|
import org.oxycblt.auxio.music.Genre
|
||||||
|
import org.oxycblt.auxio.music.Parent
|
||||||
|
import org.oxycblt.auxio.music.Song
|
||||||
|
import org.oxycblt.auxio.playback.state.LoopMode
|
||||||
|
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||||
|
import androidx.media.app.NotificationCompat as MediaNotificationCompat
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The unified notification for [PlaybackService]. This is not self-sufficient, updates have
|
||||||
|
* to be delivered manually.
|
||||||
|
* @author OxygenCobalt
|
||||||
|
*/
|
||||||
|
@SuppressLint("RestrictedApi")
|
||||||
|
class PlaybackNotification private constructor(
|
||||||
|
context: Context,
|
||||||
|
mediaToken: MediaSessionCompat.Token
|
||||||
|
) : NotificationCompat.Builder(context, CHANNEL_ID), PlaybackStateManager.Callback {
|
||||||
|
init {
|
||||||
|
val mainActivityIntent = PendingIntent.getActivity(
|
||||||
|
context, REQUEST_CODE,
|
||||||
|
Intent(context, MainActivity::class.java),
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
)
|
||||||
|
|
||||||
|
setSmallIcon(R.drawable.ic_song)
|
||||||
|
setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||||
|
setShowWhen(false)
|
||||||
|
setNotificationSilent()
|
||||||
|
setContentIntent(mainActivityIntent)
|
||||||
|
setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||||
|
|
||||||
|
addAction(buildLoopAction(context, LoopMode.NONE))
|
||||||
|
addAction(buildAction(context, ACTION_SKIP_PREV, R.drawable.ic_skip_prev))
|
||||||
|
addAction(buildPlayPauseAction(context, true))
|
||||||
|
addAction(buildAction(context, ACTION_SKIP_NEXT, R.drawable.ic_skip_next))
|
||||||
|
addAction(buildAction(context, ACTION_EXIT, R.drawable.ic_exit))
|
||||||
|
|
||||||
|
setStyle(
|
||||||
|
MediaNotificationCompat.MediaStyle()
|
||||||
|
.setMediaSession(mediaToken)
|
||||||
|
.setShowActionsInCompactView(1, 2, 3)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- STATE FUNCTIONS ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the metadata of the notification using [song].
|
||||||
|
* @param colorize Whether to show the album art of [song] on the notification
|
||||||
|
* @param onDone What to do when the loading of the album art is finished
|
||||||
|
*/
|
||||||
|
fun setMetadata(context: Context, song: Song, colorize: Boolean, onDone: () -> Unit) {
|
||||||
|
setContentTitle(song.name)
|
||||||
|
setContentText(song.album.artist.name)
|
||||||
|
|
||||||
|
// On older versions of android [API <24], show the song's album on the subtext instead of
|
||||||
|
// the current mode, as that makes more sense for the old style of media notifications.
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||||
|
setSubText(song.album.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (colorize) {
|
||||||
|
// loadBitmap() is concurrent, so only call back to the object calling this function when
|
||||||
|
// the loading is over.
|
||||||
|
loadBitmap(context, song) {
|
||||||
|
setLargeIcon(it)
|
||||||
|
|
||||||
|
onDone()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setLargeIcon(null)
|
||||||
|
|
||||||
|
onDone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the playing icon on the notification
|
||||||
|
*/
|
||||||
|
fun setPlaying(context: Context, isPlaying: Boolean) {
|
||||||
|
mActions[2] = buildPlayPauseAction(context, isPlaying)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the first action to reflect the [loopMode] given.
|
||||||
|
*/
|
||||||
|
fun setLoop(context: Context, loopMode: LoopMode) {
|
||||||
|
mActions[0] = buildLoopAction(context, loopMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the first action to reflect whether the queue is shuffled or not
|
||||||
|
*/
|
||||||
|
fun setShuffle(context: Context, isShuffling: Boolean) {
|
||||||
|
mActions[0] = buildShuffleAction(context, isShuffling)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the current [parent] to the header of the notification.
|
||||||
|
*/
|
||||||
|
fun setParent(context: Context, parent: Parent?) {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parent == null) {
|
||||||
|
// A blank parent always means that the mode is ALL_SONGS
|
||||||
|
setSubText(context.getString(R.string.label_all_songs))
|
||||||
|
} else {
|
||||||
|
if (parent is Genre) {
|
||||||
|
// Use display name for genre
|
||||||
|
setSubText(parent.displayName)
|
||||||
|
} else {
|
||||||
|
setSubText(parent.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- NOTIFICATION ACTION BUILDERS ---
|
||||||
|
|
||||||
|
private fun buildPlayPauseAction(
|
||||||
|
context: Context,
|
||||||
|
isPlaying: Boolean
|
||||||
|
): NotificationCompat.Action {
|
||||||
|
val drawableRes = if (isPlaying) R.drawable.ic_pause else R.drawable.ic_play
|
||||||
|
|
||||||
|
return buildAction(context, ACTION_PLAY_PAUSE, drawableRes)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildLoopAction(
|
||||||
|
context: Context,
|
||||||
|
loopMode: LoopMode
|
||||||
|
): NotificationCompat.Action {
|
||||||
|
val drawableRes = when (loopMode) {
|
||||||
|
LoopMode.NONE -> R.drawable.ic_loop_inactive
|
||||||
|
LoopMode.ONCE -> R.drawable.ic_loop_one
|
||||||
|
LoopMode.INFINITE -> R.drawable.ic_loop
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildAction(context, ACTION_LOOP, drawableRes)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildShuffleAction(
|
||||||
|
context: Context,
|
||||||
|
isShuffled: Boolean
|
||||||
|
): NotificationCompat.Action {
|
||||||
|
val drawableRes = if (isShuffled) R.drawable.ic_shuffle else R.drawable.ic_shuffle_inactive
|
||||||
|
|
||||||
|
return buildAction(context, ACTION_SHUFFLE, drawableRes)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildAction(
|
||||||
|
context: Context,
|
||||||
|
actionName: String,
|
||||||
|
@DrawableRes iconRes: Int
|
||||||
|
): NotificationCompat.Action {
|
||||||
|
val action = NotificationCompat.Action.Builder(
|
||||||
|
iconRes, actionName,
|
||||||
|
PendingIntent.getBroadcast(
|
||||||
|
context, REQUEST_CODE,
|
||||||
|
Intent(actionName), PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return action.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val CHANNEL_ID = "CHANNEL_AUXIO_PLAYBACK"
|
||||||
|
const val NOTIFICATION_ID = 0xA0A0
|
||||||
|
const val REQUEST_CODE = 0xA0C0
|
||||||
|
|
||||||
|
// Build type is added to the codes so that dual installations dont conflict
|
||||||
|
// with eachother.
|
||||||
|
const val ACTION_LOOP = "ACTION_AUXIO_LOOP_" + BuildConfig.BUILD_TYPE
|
||||||
|
const val ACTION_SHUFFLE = "ACTION_AUXIO_SHUFFLE_" + BuildConfig.BUILD_TYPE
|
||||||
|
const val ACTION_SKIP_PREV = "ACTION_AUXIO_SKIP_PREV_" + BuildConfig.BUILD_TYPE
|
||||||
|
const val ACTION_PLAY_PAUSE = "ACTION_AUXIO_PLAY_PAUSE_" + BuildConfig.BUILD_TYPE
|
||||||
|
const val ACTION_SKIP_NEXT = "ACTION_AUXIO_SKIP_NEXT_" + BuildConfig.BUILD_TYPE
|
||||||
|
const val ACTION_EXIT = "ACTION_AUXIO_EXIT_" + BuildConfig.BUILD_TYPE
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a new instance of [PlaybackNotification].
|
||||||
|
*/
|
||||||
|
fun from(context: Context, mediaSession: MediaSessionCompat): PlaybackNotification {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
// Create the notification channel if required.
|
||||||
|
val notificationManager = context.getSystemService(
|
||||||
|
Context.NOTIFICATION_SERVICE
|
||||||
|
) as NotificationManager
|
||||||
|
|
||||||
|
val channel = NotificationChannel(
|
||||||
|
CHANNEL_ID, context.getString(R.string.info_channel_name),
|
||||||
|
NotificationManager.IMPORTANCE_DEFAULT
|
||||||
|
)
|
||||||
|
|
||||||
|
notificationManager.createNotificationChannel(channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
return PlaybackNotification(context, mediaSession.sessionToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package org.oxycblt.auxio.playback
|
package org.oxycblt.auxio.playback.system
|
||||||
|
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
|
@ -15,7 +15,6 @@ import android.os.Parcelable
|
||||||
import android.support.v4.media.MediaMetadataCompat
|
import android.support.v4.media.MediaMetadataCompat
|
||||||
import android.support.v4.media.session.MediaSessionCompat
|
import android.support.v4.media.session.MediaSessionCompat
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import com.google.android.exoplayer2.C
|
import com.google.android.exoplayer2.C
|
||||||
import com.google.android.exoplayer2.ExoPlaybackException
|
import com.google.android.exoplayer2.ExoPlaybackException
|
||||||
import com.google.android.exoplayer2.MediaItem
|
import com.google.android.exoplayer2.MediaItem
|
||||||
|
@ -40,10 +39,10 @@ import kotlinx.coroutines.flow.takeWhile
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.oxycblt.auxio.coil.loadBitmap
|
import org.oxycblt.auxio.coil.loadBitmap
|
||||||
import org.oxycblt.auxio.logD
|
import org.oxycblt.auxio.logD
|
||||||
|
import org.oxycblt.auxio.music.Parent
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.toURI
|
import org.oxycblt.auxio.music.toURI
|
||||||
import org.oxycblt.auxio.playback.state.LoopMode
|
import org.oxycblt.auxio.playback.state.LoopMode
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackMode
|
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||||
import org.oxycblt.auxio.settings.SettingsManager
|
import org.oxycblt.auxio.settings.SettingsManager
|
||||||
|
|
||||||
|
@ -73,7 +72,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
private lateinit var notificationManager: NotificationManager
|
private lateinit var notificationManager: NotificationManager
|
||||||
private lateinit var notification: NotificationCompat.Builder
|
private lateinit var notification: PlaybackNotification
|
||||||
|
|
||||||
private lateinit var audioReactor: AudioReactor
|
private lateinit var audioReactor: AudioReactor
|
||||||
private var isForeground = false
|
private var isForeground = false
|
||||||
|
@ -124,12 +123,12 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
|
||||||
systemReceiver = SystemEventReceiver()
|
systemReceiver = SystemEventReceiver()
|
||||||
|
|
||||||
IntentFilter().apply {
|
IntentFilter().apply {
|
||||||
addAction(NotificationUtils.ACTION_LOOP)
|
addAction(PlaybackNotification.ACTION_LOOP)
|
||||||
addAction(NotificationUtils.ACTION_SHUFFLE)
|
addAction(PlaybackNotification.ACTION_SHUFFLE)
|
||||||
addAction(NotificationUtils.ACTION_SKIP_PREV)
|
addAction(PlaybackNotification.ACTION_SKIP_PREV)
|
||||||
addAction(NotificationUtils.ACTION_PLAY_PAUSE)
|
addAction(PlaybackNotification.ACTION_PLAY_PAUSE)
|
||||||
addAction(NotificationUtils.ACTION_SKIP_NEXT)
|
addAction(PlaybackNotification.ACTION_SKIP_NEXT)
|
||||||
addAction(NotificationUtils.ACTION_EXIT)
|
addAction(PlaybackNotification.ACTION_EXIT)
|
||||||
|
|
||||||
addAction(BluetoothDevice.ACTION_ACL_CONNECTED)
|
addAction(BluetoothDevice.ACTION_ACL_CONNECTED)
|
||||||
addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED)
|
addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED)
|
||||||
|
@ -142,7 +141,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
|
||||||
// --- NOTIFICATION SETUP ---
|
// --- NOTIFICATION SETUP ---
|
||||||
|
|
||||||
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
notification = notificationManager.createMediaNotification(this, mediaSession)
|
notification = PlaybackNotification.from(this, mediaSession)
|
||||||
|
|
||||||
// --- PLAYBACKSTATEMANAGER SETUP ---
|
// --- PLAYBACKSTATEMANAGER SETUP ---
|
||||||
|
|
||||||
|
@ -235,8 +234,8 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
|
||||||
stopForegroundAndNotification()
|
stopForegroundAndNotification()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onModeUpdate(mode: PlaybackMode) {
|
override fun onParentUpdate(parent: Parent?) {
|
||||||
notification.updateMode(this)
|
notification.setParent(this, parent)
|
||||||
|
|
||||||
startForegroundOrNotify()
|
startForegroundOrNotify()
|
||||||
}
|
}
|
||||||
|
@ -244,39 +243,39 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
|
||||||
override fun onPlayingUpdate(isPlaying: Boolean) {
|
override fun onPlayingUpdate(isPlaying: Boolean) {
|
||||||
if (isPlaying && !player.isPlaying) {
|
if (isPlaying && !player.isPlaying) {
|
||||||
player.play()
|
player.play()
|
||||||
notification.updatePlaying(this)
|
|
||||||
audioReactor.requestFocus()
|
audioReactor.requestFocus()
|
||||||
startForegroundOrNotify()
|
|
||||||
|
|
||||||
startPollingPosition()
|
startPollingPosition()
|
||||||
} else {
|
} else {
|
||||||
player.pause()
|
player.pause()
|
||||||
notification.updatePlaying(this)
|
|
||||||
startForegroundOrNotify()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLoopUpdate(mode: LoopMode) {
|
notification.setPlaying(this, isPlaying)
|
||||||
when (mode) {
|
startForegroundOrNotify()
|
||||||
LoopMode.NONE -> {
|
|
||||||
player.repeatMode = Player.REPEAT_MODE_OFF
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
player.repeatMode = Player.REPEAT_MODE_ONE
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
notification.updateExtraAction(this, settingsManager.useAltNotifAction)
|
override fun onLoopUpdate(loopMode: LoopMode) {
|
||||||
|
player.repeatMode = if (loopMode == LoopMode.NONE) {
|
||||||
|
Player.REPEAT_MODE_OFF
|
||||||
|
} else {
|
||||||
|
Player.REPEAT_MODE_ONE
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!settingsManager.useAltNotifAction) {
|
||||||
|
notification.setLoop(this, loopMode)
|
||||||
|
|
||||||
startForegroundOrNotify()
|
startForegroundOrNotify()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onShuffleUpdate(isShuffling: Boolean) {
|
override fun onShuffleUpdate(isShuffling: Boolean) {
|
||||||
if (settingsManager.useAltNotifAction) {
|
if (!settingsManager.useAltNotifAction) {
|
||||||
notification.updateExtraAction(this, settingsManager.useAltNotifAction)
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
notification.setShuffle(this, isShuffling)
|
||||||
|
|
||||||
startForegroundOrNotify()
|
startForegroundOrNotify()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSeek(position: Long) {
|
override fun onSeek(position: Long) {
|
||||||
player.seekTo(position)
|
player.seekTo(position)
|
||||||
|
@ -293,7 +292,11 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNotifActionUpdate(useAltAction: Boolean) {
|
override fun onNotifActionUpdate(useAltAction: Boolean) {
|
||||||
notification.updateExtraAction(this, useAltAction)
|
if (useAltAction) {
|
||||||
|
notification.setShuffle(this, playbackManager.isShuffling)
|
||||||
|
} else {
|
||||||
|
notification.setLoop(this, playbackManager.loopMode)
|
||||||
|
}
|
||||||
|
|
||||||
startForegroundOrNotify()
|
startForegroundOrNotify()
|
||||||
}
|
}
|
||||||
|
@ -359,12 +362,17 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
|
||||||
* Restore the notification, if the service was destroyed while [PlaybackStateManager] persisted.
|
* Restore the notification, if the service was destroyed while [PlaybackStateManager] persisted.
|
||||||
*/
|
*/
|
||||||
private fun restoreNotification() {
|
private fun restoreNotification() {
|
||||||
notification.updateExtraAction(this, settingsManager.useAltNotifAction)
|
notification.setParent(this, playbackManager.parent)
|
||||||
notification.updateMode(this)
|
notification.setPlaying(this, playbackManager.isPlaying)
|
||||||
notification.updatePlaying(this)
|
|
||||||
|
|
||||||
playbackManager.song?.let {
|
if (settingsManager.useAltNotifAction) {
|
||||||
notification.setMetadata(this, it, settingsManager.colorizeNotif) {
|
notification.setShuffle(this, playbackManager.isShuffling)
|
||||||
|
} else {
|
||||||
|
notification.setLoop(this, playbackManager.loopMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
playbackManager.song?.let { song ->
|
||||||
|
notification.setMetadata(this, song, settingsManager.colorizeNotif) {
|
||||||
if (playbackManager.isPlaying) {
|
if (playbackManager.isPlaying) {
|
||||||
startForegroundOrNotify()
|
startForegroundOrNotify()
|
||||||
} else {
|
} else {
|
||||||
|
@ -427,14 +435,18 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
|
||||||
if (!isForeground) {
|
if (!isForeground) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
startForeground(
|
startForeground(
|
||||||
NotificationUtils.NOTIFICATION_ID, notification.build(),
|
PlaybackNotification.NOTIFICATION_ID, notification.build(),
|
||||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
|
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
startForeground(NotificationUtils.NOTIFICATION_ID, notification.build())
|
startForeground(
|
||||||
|
PlaybackNotification.NOTIFICATION_ID, notification.build()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
notificationManager.notify(NotificationUtils.NOTIFICATION_ID, notification.build())
|
notificationManager.notify(
|
||||||
|
PlaybackNotification.NOTIFICATION_ID, notification.build()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -444,7 +456,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
|
||||||
*/
|
*/
|
||||||
private fun stopForegroundAndNotification() {
|
private fun stopForegroundAndNotification() {
|
||||||
stopForeground(true)
|
stopForeground(true)
|
||||||
notificationManager.cancel(NotificationUtils.NOTIFICATION_ID)
|
notificationManager.cancel(PlaybackNotification.NOTIFICATION_ID)
|
||||||
|
|
||||||
isForeground = false
|
isForeground = false
|
||||||
}
|
}
|
||||||
|
@ -504,19 +516,19 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
|
||||||
|
|
||||||
action?.let {
|
action?.let {
|
||||||
when (it) {
|
when (it) {
|
||||||
NotificationUtils.ACTION_LOOP ->
|
PlaybackNotification.ACTION_LOOP ->
|
||||||
playbackManager.setLoopMode(playbackManager.loopMode.increment())
|
playbackManager.setLoopMode(playbackManager.loopMode.increment())
|
||||||
|
|
||||||
NotificationUtils.ACTION_SHUFFLE ->
|
PlaybackNotification.ACTION_SHUFFLE ->
|
||||||
playbackManager.setShuffling(!playbackManager.isShuffling, keepSong = true)
|
playbackManager.setShuffling(!playbackManager.isShuffling, keepSong = true)
|
||||||
|
|
||||||
NotificationUtils.ACTION_SKIP_PREV -> playbackManager.prev()
|
PlaybackNotification.ACTION_SKIP_PREV -> playbackManager.prev()
|
||||||
|
|
||||||
NotificationUtils.ACTION_PLAY_PAUSE ->
|
PlaybackNotification.ACTION_PLAY_PAUSE ->
|
||||||
playbackManager.setPlaying(!playbackManager.isPlaying)
|
playbackManager.setPlaying(!playbackManager.isPlaying)
|
||||||
|
|
||||||
NotificationUtils.ACTION_SKIP_NEXT -> playbackManager.next()
|
PlaybackNotification.ACTION_SKIP_NEXT -> playbackManager.next()
|
||||||
NotificationUtils.ACTION_EXIT -> stop()
|
PlaybackNotification.ACTION_EXIT -> stop()
|
||||||
|
|
||||||
BluetoothDevice.ACTION_ACL_CONNECTED -> resume()
|
BluetoothDevice.ACTION_ACL_CONNECTED -> resume()
|
||||||
BluetoothDevice.ACTION_ACL_DISCONNECTED -> pause()
|
BluetoothDevice.ACTION_ACL_DISCONNECTED -> pause()
|
|
@ -1,6 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<!-- Info namespace | App labels -->
|
<!-- Info namespace | App labels -->
|
||||||
|
<string name="info_app_desc">Ein einfacher, sinnvoller Musikplayer für android.</string>
|
||||||
<string name="info_channel_name">Musikwiedergabe</string>
|
<string name="info_channel_name">Musikwiedergabe</string>
|
||||||
<string name="info_service_desc">der Musikwiedergabedienst von Auxio.</string>
|
<string name="info_service_desc">der Musikwiedergabedienst von Auxio.</string>
|
||||||
|
|
||||||
|
|
|
@ -45,7 +45,8 @@ org.oxycblt.auxio # Main UI's and logging utilities
|
||||||
│ └──.processing # Systems for music loading and organization
|
│ └──.processing # Systems for music loading and organization
|
||||||
├──.playback # Playback UI and systems
|
├──.playback # Playback UI and systems
|
||||||
│ ├──.queue # Queue user interface
|
│ ├──.queue # Queue user interface
|
||||||
│ └──.state # Backend/Modes for the playback state
|
│ ├──.state # Backend/Modes for the playback state
|
||||||
|
│ └──.system # System-side playback [Services, ExoPlayer]
|
||||||
├──.recycler # Shared RecyclerView utilities and modes
|
├──.recycler # Shared RecyclerView utilities and modes
|
||||||
│ └──.viewholders # Shared ViewHolders and ViewHolder utilities
|
│ └──.viewholders # Shared ViewHolders and ViewHolder utilities
|
||||||
├──.search # Search UI
|
├──.search # Search UI
|
||||||
|
|
Loading…
Reference in a new issue