diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index d858553c4..f422c3a12 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -16,6 +16,7 @@
@@ -26,7 +27,8 @@
()
val songs: List get() = mSongs
+ var loaded = false
+ private set
+
// Load/Sort the entire library.
// ONLY CALL THIS FROM AN IO THREAD.
fun load(app: Application): MusicLoaderResponse {
@@ -69,6 +72,10 @@ class MusicStore private constructor() {
)
}
+ if (loader.response == MusicLoaderResponse.DONE) {
+ loaded = true
+ }
+
return loader.response
}
diff --git a/app/src/main/java/org/oxycblt/auxio/playback/NotificationUtils.kt b/app/src/main/java/org/oxycblt/auxio/playback/NotificationUtils.kt
new file mode 100644
index 000000000..03ccb1ece
--- /dev/null
+++ b/app/src/main/java/org/oxycblt/auxio/playback/NotificationUtils.kt
@@ -0,0 +1,152 @@
+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.MainActivity
+import org.oxycblt.auxio.R
+import org.oxycblt.auxio.music.Song
+import org.oxycblt.auxio.music.coil.getBitmap
+import org.oxycblt.auxio.playback.state.LoopMode
+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
+
+ const val ACTION_LOOP = "ACTION_AUXIO_LOOP"
+ const val ACTION_SKIP_PREV = "ACTION_AUXIO_SKIP_PREV"
+ const val ACTION_PLAY_PAUSE = "ACTION_AUXIO_PLAY_PAUSE"
+ const val ACTION_SKIP_NEXT = "ACTION_AUXIO_SKIP_NEXT"
+ const val ACTION_SHUFFLE = "ACTION_AUXIO_SHUFFLE"
+}
+
+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.label_notification_playback),
+ NotificationManager.IMPORTANCE_DEFAULT
+ )
+
+ createNotificationChannel(channel)
+ }
+
+ val mainIntent = PendingIntent.getActivity(
+ context, NotificationUtils.REQUEST_CODE,
+ Intent(context, MainActivity::class.java),
+ PendingIntent.FLAG_UPDATE_CURRENT
+ )
+
+ // TODO: It would be cool if the notification intent took you to the now playing screen.
+ 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)
+ .setTicker(context.getString(R.string.title_playback))
+ .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_SHUFFLE, context))
+ .setSubText(context.getString(R.string.title_playback))
+ .setContentIntent(mainIntent)
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+}
+
+fun NotificationCompat.Builder.setMetadata(song: Song, context: Context, onDone: () -> Unit) {
+ setContentTitle(song.name)
+ setContentText(
+ song.album.artist.name,
+ )
+
+ getBitmap(song, context) {
+ setLargeIcon(it)
+
+ onDone()
+ }
+}
+
+@SuppressLint("RestrictedApi")
+fun NotificationCompat.Builder.updateLoop(context: Context) {
+ mActions[0] = newAction(NotificationUtils.ACTION_LOOP, context)
+}
+
+@SuppressLint("RestrictedApi")
+fun NotificationCompat.Builder.updatePlaying(context: Context) {
+ mActions[2] = newAction(NotificationUtils.ACTION_PLAY_PAUSE, context)
+}
+
+@SuppressLint("RestrictedApi")
+fun NotificationCompat.Builder.updateShuffle(context: Context) {
+ mActions[4] = newAction(NotificationUtils.ACTION_SHUFFLE, context)
+}
+
+private fun newAction(action: String, context: Context): NotificationCompat.Action {
+ val playbackManager = PlaybackStateManager.getInstance()
+
+ val drawable = when (action) {
+ NotificationUtils.ACTION_LOOP -> {
+ when (playbackManager.loopMode) {
+ LoopMode.NONE -> R.drawable.ic_loop_disabled
+ LoopMode.ONCE -> R.drawable.ic_loop_one
+ LoopMode.INFINITE -> R.drawable.ic_loop
+ }
+ }
+
+ 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_SHUFFLE -> {
+ if (playbackManager.isShuffling) {
+ R.drawable.ic_shuffle
+ } else {
+ R.drawable.ic_shuffle_disabled
+ }
+ }
+
+ else -> R.drawable.ic_play
+ }
+
+ return NotificationCompat.Action.Builder(
+ drawable, action, newPlaybackIntent(action, context)
+ ).build()
+}
+
+private fun newPlaybackIntent(action: String, context: Context): PendingIntent {
+ return PendingIntent.getBroadcast(
+ context, NotificationUtils.REQUEST_CODE, Intent(action), PendingIntent.FLAG_UPDATE_CURRENT
+ )
+}
diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackNotificationHolder.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackNotificationHolder.kt
deleted file mode 100644
index 1d1fb3c7f..000000000
--- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackNotificationHolder.kt
+++ /dev/null
@@ -1,208 +0,0 @@
-package org.oxycblt.auxio.playback
-
-import android.annotation.SuppressLint
-import android.app.Notification
-import android.app.NotificationChannel
-import android.app.NotificationManager
-import android.app.PendingIntent
-import android.content.Context
-import android.content.Intent
-import android.content.pm.ServiceInfo
-import android.os.Build
-import android.support.v4.media.session.MediaSessionCompat
-import android.util.Log
-import androidx.core.app.NotificationCompat
-import androidx.media.app.NotificationCompat.MediaStyle
-import org.oxycblt.auxio.R
-import org.oxycblt.auxio.music.Song
-import org.oxycblt.auxio.music.coil.getBitmap
-import org.oxycblt.auxio.playback.state.LoopMode
-import org.oxycblt.auxio.playback.state.PlaybackStateManager
-
-// Holder for the playback notification, should only be used by PlaybackService.
-// TODO: You really need to rewrite this class. Christ.
-// TODO: Disable skip prev/next buttons when you cant do those actions
-// TODO: Implement a way to exit the notification
-class PlaybackNotificationHolder {
- private lateinit var mNotification: Notification
-
- private lateinit var notificationManager: NotificationManager
- private lateinit var baseNotification: NotificationCompat.Builder
-
- private val playbackManager = PlaybackStateManager.getInstance()
-
- private var isForeground = false
-
- fun init(context: Context, session: MediaSessionCompat, playbackService: PlaybackService) {
- // Never run if the notification has already been created
- if (!::mNotification.isInitialized) {
- notificationManager =
- context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
-
- // Create a notification channel if required
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- val channel = NotificationChannel(
- CHANNEL_ID,
- context.getString(R.string.label_notification_playback),
- NotificationManager.IMPORTANCE_DEFAULT
- )
- notificationManager.createNotificationChannel(channel)
- }
-
- baseNotification = NotificationCompat.Builder(context, CHANNEL_ID)
- .setSmallIcon(R.drawable.ic_song)
- .setStyle(
- MediaStyle()
- .setMediaSession(session.sessionToken)
- .setShowActionsInCompactView(1, 2, 3)
- )
- .setCategory(NotificationCompat.CATEGORY_SERVICE)
- .setChannelId(CHANNEL_ID)
- .setShowWhen(false)
- .setTicker(playbackService.getString(R.string.title_playback))
- .addAction(createAction(ACTION_LOOP, playbackService))
- .addAction(createAction(ACTION_SKIP_PREV, playbackService))
- .addAction(createAction(ACTION_PLAY_PAUSE, playbackService))
- .addAction(createAction(ACTION_SKIP_NEXT, playbackService))
- .addAction(createAction(ACTION_SHUFFLE, playbackService))
- .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
-
- mNotification = baseNotification.build()
- }
- }
-
- fun setMetadata(song: Song, playbackService: PlaybackService) {
- // Set the basic metadata since MediaStyle wont do it yourself.
- // Fun Fact: The documentation still says that MediaStyle will handle metadata changes
- // from MediaSession, even though it doesn't. Its been 6 years. Fun.
- baseNotification
- .setContentTitle(song.name)
- .setContentText(
- playbackService.getString(
- R.string.format_info,
- song.album.artist.name,
- song.album.name
- )
- )
-
- getBitmap(song, playbackService) {
- baseNotification.setLargeIcon(it)
-
- startForegroundOrNotify(playbackService)
- }
- }
-
- @SuppressLint("RestrictedApi")
- fun updatePlaying(playbackService: PlaybackService) {
- baseNotification.mActions[2] = createAction(ACTION_PLAY_PAUSE, playbackService)
-
- Log.d(this::class.simpleName, baseNotification.mActions[1].iconCompat?.resId.toString())
-
- startForegroundOrNotify(playbackService)
- }
-
- @SuppressLint("RestrictedApi")
- fun updateLoop(playbackService: PlaybackService) {
- baseNotification.mActions[0] = createAction(ACTION_LOOP, playbackService)
-
- startForegroundOrNotify(playbackService)
- }
-
- @SuppressLint("RestrictedApi")
- fun updateShuffle(playbackService: PlaybackService) {
- baseNotification.mActions[4] = createAction(ACTION_SHUFFLE, playbackService)
-
- startForegroundOrNotify(playbackService)
- }
-
- fun stop(playbackService: PlaybackService) {
- playbackService.stopForeground(true)
- notificationManager.cancel(NOTIFICATION_ID)
-
- isForeground = false
- }
-
- private fun createAction(action: String, playbackService: PlaybackService): NotificationCompat.Action {
- val drawable = when (action) {
- ACTION_LOOP -> {
- when (playbackManager.loopMode) {
- LoopMode.NONE -> R.drawable.ic_loop_disabled
- LoopMode.ONCE -> R.drawable.ic_loop_one
- LoopMode.INFINITE -> R.drawable.ic_loop
- }
- }
-
- ACTION_SKIP_PREV -> {
- R.drawable.ic_skip_prev
- }
-
- ACTION_PLAY_PAUSE -> {
- if (playbackManager.isPlaying) {
- R.drawable.ic_pause
- } else {
- R.drawable.ic_play
- }
- }
-
- ACTION_SKIP_NEXT -> {
- R.drawable.ic_skip_next
- }
-
- ACTION_SHUFFLE -> {
- if (playbackManager.isShuffling) {
- R.drawable.ic_shuffle
- } else {
- R.drawable.ic_shuffle_disabled
- }
- }
-
- else -> R.drawable.ic_play
- }
-
- return NotificationCompat.Action.Builder(
- drawable, action, createPlaybackAction(action, playbackService)
- ).build()
- }
-
- private fun createPlaybackAction(action: String, playbackService: PlaybackService): PendingIntent {
- val intent = Intent()
- intent.action = action
-
- return PendingIntent.getBroadcast(
- playbackService,
- REQUEST_CODE,
- intent,
- PendingIntent.FLAG_UPDATE_CURRENT
- )
- }
-
- private fun startForegroundOrNotify(playbackService: PlaybackService) {
- mNotification = baseNotification.build()
-
- // Start the service in the foreground if haven't already.
- if (!isForeground) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- playbackService.startForeground(
- NOTIFICATION_ID, mNotification,
- ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
- )
- } else {
- playbackService.startForeground(NOTIFICATION_ID, mNotification)
- }
- } else {
- notificationManager.notify(NOTIFICATION_ID, mNotification)
- }
- }
-
- companion object {
- const val CHANNEL_ID = "CHANNEL_AUXIO_PLAYBACK"
- const val NOTIFICATION_ID = 0xA0A0
- const val REQUEST_CODE = 0xA0C0
-
- const val ACTION_LOOP = "ACTION_AUXIO_LOOP"
- const val ACTION_SKIP_PREV = "ACTION_AUXIO_SKIP_PREV"
- const val ACTION_PLAY_PAUSE = "ACTION_AUXIO_PLAY_PAUSE"
- const val ACTION_SKIP_NEXT = "ACTION_AUXIO_SKIP_NEXT"
- const val ACTION_SHUFFLE = "ACTION_AUXIO_SHUFFLE"
- }
-}
diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackService.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackService.kt
index 22e819133..7ad5bd241 100644
--- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackService.kt
+++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackService.kt
@@ -1,11 +1,13 @@
package org.oxycblt.auxio.playback
+import android.app.NotificationManager
import android.app.Service
import android.bluetooth.BluetoothDevice
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
+import android.content.pm.ServiceInfo
import android.media.AudioManager
import android.os.Build
import android.os.IBinder
@@ -14,6 +16,7 @@ import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaSessionCompat
import android.util.Log
import android.view.KeyEvent
+import androidx.core.app.NotificationCompat
import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.ExoPlaybackException
import com.google.android.exoplayer2.MediaItem
@@ -47,9 +50,12 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
private val playbackManager = PlaybackStateManager.getInstance()
private lateinit var mediaSession: MediaSessionCompat
private lateinit var systemReceiver: SystemEventReceiver
- private lateinit var notificationHolder: PlaybackNotificationHolder
+
+ private lateinit var notificationManager: NotificationManager
+ private lateinit var notification: NotificationCompat.Builder
private var changeIsFromAudioFocus = true
+ private var isForeground = false
private val serviceJob = Job()
private val serviceScope = CoroutineScope(
@@ -103,11 +109,12 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
// Set up callback for system events
systemReceiver = SystemEventReceiver()
IntentFilter().apply {
- addAction(PlaybackNotificationHolder.ACTION_LOOP)
- addAction(PlaybackNotificationHolder.ACTION_SKIP_PREV)
- addAction(PlaybackNotificationHolder.ACTION_PLAY_PAUSE)
- addAction(PlaybackNotificationHolder.ACTION_SKIP_NEXT)
- addAction(PlaybackNotificationHolder.ACTION_SHUFFLE)
+ addAction(NotificationUtils.ACTION_LOOP)
+ addAction(NotificationUtils.ACTION_SKIP_PREV)
+ addAction(NotificationUtils.ACTION_PLAY_PAUSE)
+ addAction(NotificationUtils.ACTION_SKIP_NEXT)
+ addAction(NotificationUtils.ACTION_SHUFFLE)
+
addAction(BluetoothDevice.ACTION_ACL_CONNECTED)
addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED)
addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
@@ -118,9 +125,9 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
// --- NOTIFICATION SETUP ---
- notificationHolder = PlaybackNotificationHolder()
+ notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
- notificationHolder.init(applicationContext, mediaSession, this)
+ notification = notificationManager.createMediaNotification(this, mediaSession)
// --- PLAYBACKSTATEMANAGER SETUP ---
@@ -134,7 +141,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
override fun onDestroy() {
super.onDestroy()
- notificationHolder.stop(this)
+ stopForegroundAndNotification()
unregisterReceiver(systemReceiver)
// Release everything that could cause a memory leak if left around
@@ -199,14 +206,16 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
player.play()
uploadMetadataToSession(it)
- notificationHolder.setMetadata(playbackManager.song!!, this)
+ notification.setMetadata(playbackManager.song!!, this) {
+ startForegroundOrNotify()
+ }
return
}
// Stop playing/the notification if there's nothing to play.
player.stop()
- notificationHolder.stop(this)
+ stopForegroundAndNotification()
}
override fun onPlayingUpdate(isPlaying: Boolean) {
@@ -214,18 +223,23 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
if (isPlaying && !player.isPlaying) {
player.play()
- notificationHolder.updatePlaying(this)
+ notification.updatePlaying(this)
+ startForegroundOrNotify()
+
startPollingPosition()
} else {
player.pause()
- notificationHolder.updatePlaying(this)
+
+ notification.updatePlaying(this)
+ startForegroundOrNotify()
}
}
override fun onShuffleUpdate(isShuffling: Boolean) {
changeIsFromAudioFocus = false
- notificationHolder.updateShuffle(this)
+ notification.updateShuffle(this)
+ startForegroundOrNotify()
}
override fun onLoopUpdate(mode: LoopMode) {
@@ -240,7 +254,8 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
}
}
- notificationHolder.updateLoop(this)
+ notification.updateLoop(this)
+ startForegroundOrNotify()
}
override fun onSeekConfirm(position: Long) {
@@ -258,7 +273,9 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
player.prepare()
player.seekTo(playbackManager.position)
- notificationHolder.setMetadata(it, this)
+ notification.setMetadata(it, this) {
+ startForegroundOrNotify()
+ }
}
}
@@ -297,6 +314,29 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
}
}
+ private fun startForegroundOrNotify() {
+ // Start the service in the foreground if haven't already.
+ if (!isForeground) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ startForeground(
+ NotificationUtils.NOTIFICATION_ID, notification.build(),
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
+ )
+ } else {
+ startForeground(NotificationUtils.NOTIFICATION_ID, notification.build())
+ }
+ } else {
+ notificationManager.notify(NotificationUtils.NOTIFICATION_ID, notification.build())
+ }
+ }
+
+ private fun stopForegroundAndNotification() {
+ stopForeground(true)
+ notificationManager.cancel(NotificationUtils.NOTIFICATION_ID)
+
+ isForeground = false
+ }
+
// Handle a media button event.
private fun handleMediaButtonEvent(event: Intent): Boolean {
val item = event
@@ -344,13 +384,13 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
action?.let {
when (it) {
- PlaybackNotificationHolder.ACTION_LOOP ->
+ NotificationUtils.ACTION_LOOP ->
playbackManager.setLoopMode(playbackManager.loopMode.increment())
- PlaybackNotificationHolder.ACTION_SKIP_PREV -> playbackManager.prev()
- PlaybackNotificationHolder.ACTION_PLAY_PAUSE ->
+ NotificationUtils.ACTION_SKIP_PREV -> playbackManager.prev()
+ NotificationUtils.ACTION_PLAY_PAUSE ->
playbackManager.setPlayingStatus(!playbackManager.isPlaying)
- PlaybackNotificationHolder.ACTION_SKIP_NEXT -> playbackManager.next()
- PlaybackNotificationHolder.ACTION_SHUFFLE ->
+ NotificationUtils.ACTION_SKIP_NEXT -> playbackManager.next()
+ NotificationUtils.ACTION_SHUFFLE ->
playbackManager.setShuffleStatus(!playbackManager.isShuffling)
BluetoothDevice.ACTION_ACL_CONNECTED -> resume()