Unify notification

Unify the notification extensions into a single PlaybackNotification object that does not rely on PlaybackStateManager and SettingsManager.
This commit is contained in:
OxygenCobalt 2021-02-19 15:54:27 -07:00
parent ca4cabedbc
commit 7524589969
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
11 changed files with 295 additions and 280 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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,40 +243,40 @@ 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) {
when (mode) {
LoopMode.NONE -> {
player.repeatMode = Player.REPEAT_MODE_OFF
}
else -> {
player.repeatMode = Player.REPEAT_MODE_ONE
}
} }
notification.updateExtraAction(this, settingsManager.useAltNotifAction) notification.setPlaying(this, isPlaying)
startForegroundOrNotify() startForegroundOrNotify()
} }
override fun onShuffleUpdate(isShuffling: Boolean) { override fun onLoopUpdate(loopMode: LoopMode) {
if (settingsManager.useAltNotifAction) { player.repeatMode = if (loopMode == LoopMode.NONE) {
notification.updateExtraAction(this, settingsManager.useAltNotifAction) Player.REPEAT_MODE_OFF
} else {
Player.REPEAT_MODE_ONE
}
if (!settingsManager.useAltNotifAction) {
notification.setLoop(this, loopMode)
startForegroundOrNotify() startForegroundOrNotify()
} }
} }
override fun onShuffleUpdate(isShuffling: Boolean) {
if (!settingsManager.useAltNotifAction) {
return
}
notification.setShuffle(this, isShuffling)
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()

View file

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

View file

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