diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 3b33222b0..d69adda0c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -26,7 +26,6 @@
+ android:stopWithTask="false" />
-
\ No newline at end of file
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 bd0592657..a326598c3 100644
--- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackService.kt
+++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackService.kt
@@ -1,10 +1,15 @@
package org.oxycblt.auxio.playback
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
import android.app.Service
+import android.content.Context
import android.content.Intent
-import android.os.Binder
import android.os.Build
import android.os.IBinder
+import android.util.Log
+import androidx.core.app.NotificationCompat
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.SimpleExoPlayer
@@ -17,11 +22,15 @@ import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.launch
+import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.toURI
import org.oxycblt.auxio.playback.state.PlaybackStateCallback
import org.oxycblt.auxio.playback.state.PlaybackStateManager
+private const val CHANNEL_ID = "CHANNEL_AUXIO_PLAYBACK"
+private const val NOTIF_ID = 0xA0A0
+
// A Service that manages the single ExoPlayer instance and [attempts] to keep
// persistence if the app closes.
class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
@@ -34,7 +43,6 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
p
}
- private val mBinder = LocalBinder()
private val playbackManager = PlaybackStateManager.getInstance()
private val serviceJob = Job()
@@ -42,13 +50,25 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
serviceJob + Dispatchers.Main
)
- override fun onBind(intent: Intent): IBinder {
- return mBinder
+ private var isForeground = false
+
+ private lateinit var notification: Notification
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ Log.d(this::class.simpleName, "Service is active.")
+
+ return START_STICKY
+ }
+
+ override fun onBind(intent: Intent): IBinder? {
+ return null
}
override fun onCreate() {
super.onCreate()
+ notification = createNotification()
+
playbackManager.addCallback(this)
}
@@ -58,6 +78,8 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
player.release()
serviceJob.cancel()
playbackManager.removeCallback(this)
+
+ stopForeground(true)
}
override fun onPlaybackStateChanged(state: Int) {
@@ -69,8 +91,14 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
}
override fun onSongUpdate(song: Song?) {
- song?.let { song ->
- val item = MediaItem.fromUri(song.id.toURI())
+ song?.let {
+ if (!isForeground) {
+ startForeground(NOTIF_ID, notification)
+
+ isForeground = true
+ }
+
+ val item = MediaItem.fromUri(it.id.toURI())
player.setMediaItem(item)
player.prepare()
player.play()
@@ -87,13 +115,13 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
}
}
- fun doSeek(position: Long) {
+ override fun onSeekConfirm(position: Long) {
player.seekTo(position * 1000)
}
// Awful Hack to get position polling to work, as exoplayer does not provide any
// onPositionChanged callback for some inane reason.
- // FIXME: Consider using exoplayer UI elements here, don't be surprised if this causes problems.
+ // FIXME: Don't be surprised if this causes problems.
private fun pollCurrentPosition() = flow {
while (player.currentPosition <= player.duration) {
@@ -110,7 +138,29 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
}
}
- inner class LocalBinder : Binder() {
- fun getService() = this@PlaybackService
+ private fun createNotification(): Notification {
+ val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val channel = NotificationChannel(
+ CHANNEL_ID,
+ getString(R.string.description_playback),
+ NotificationManager.IMPORTANCE_DEFAULT
+ )
+ notificationManager.createNotificationChannel(channel)
+ }
+ // TODO: Placeholder, implement proper media controls :)
+ val notif = NotificationCompat.Builder(
+ applicationContext,
+ CHANNEL_ID
+ )
+ .setSmallIcon(R.drawable.ic_song)
+ .setContentTitle(getString(R.string.app_name))
+ .setContentText(getString(R.string.description_playback))
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+ .setChannelId(CHANNEL_ID)
+ .build()
+
+ return notif
}
}
diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt
index d25e146c9..b4fb6a574 100644
--- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt
+++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt
@@ -1,10 +1,7 @@
package org.oxycblt.auxio.playback
-import android.content.ComponentName
import android.content.Context
import android.content.Intent
-import android.content.ServiceConnection
-import android.os.IBinder
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
@@ -68,25 +65,22 @@ class PlaybackViewModel(private val context: Context) : ViewModel(), PlaybackSta
// Service setup
private val playbackManager = PlaybackStateManager.getInstance()
- private lateinit var playbackService: PlaybackService
- private var playbackIntent: Intent
-
- private val connection = object : ServiceConnection {
- override fun onServiceConnected(name: ComponentName, binder: IBinder) {
- playbackService = (binder as PlaybackService.LocalBinder).getService()
- }
-
- override fun onServiceDisconnected(name: ComponentName) {
- Log.d(this::class.simpleName, "Service disconnected.")
- }
- }
-
init {
- playbackIntent = Intent(context, PlaybackService::class.java).also {
- context.bindService(it, connection, Context.BIND_AUTO_CREATE)
+ // Start the service from the ViewModel.
+ // Yes, I know ViewModels aren't supposed to deal with this stuff but for some
+ // reason it only works here.
+ Intent(context, PlaybackService::class.java).also {
+ context.startService(it)
}
playbackManager.addCallback(this)
+
+ // If the PlaybackViewModel was cleared [signified by the PlaybackStateManager having a
+ // song and the fact that were are in the init function], then try to restore the playback
+ // state.
+ if (playbackManager.song != null) {
+ restorePlaybackState()
+ }
}
// --- PLAYING FUNCTIONS ---
@@ -143,11 +137,8 @@ class PlaybackViewModel(private val context: Context) : ViewModel(), PlaybackSta
}
// Update the position and push the change the playbackManager.
- // This is done when the seek is confirmed to make playbackService seek to the position.
fun updatePosition(progress: Int) {
- playbackManager.setPosition(progress.toLong())
-
- playbackService.doSeek(progress.toLong())
+ playbackManager.seekTo(progress.toLong())
}
// --- QUEUE FUNCTIONS ---
@@ -241,6 +232,15 @@ class PlaybackViewModel(private val context: Context) : ViewModel(), PlaybackSta
mIsShuffling.value = isShuffling
}
+ private fun restorePlaybackState() {
+ mSong.value = playbackManager.song
+ mPosition.value = playbackManager.position
+ mQueue.value = playbackManager.queue
+ mIndex.value = playbackManager.index
+ mIsPlaying.value = playbackManager.isPlaying
+ mIsShuffling.value = playbackManager.isShuffling
+ }
+
class Factory(private val context: Context) : ViewModelProvider.Factory {
@Suppress("unchecked_cast")
override fun create(modelClass: Class): T {
diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateCallback.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateCallback.kt
index 94ed7eebb..e2272741b 100644
--- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateCallback.kt
+++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateCallback.kt
@@ -9,4 +9,5 @@ interface PlaybackStateCallback {
fun onPlayingUpdate(isPlaying: Boolean) {}
fun onShuffleUpdate(isShuffling: Boolean) {}
fun onIndexUpdate(index: Int) {}
+ fun onSeekConfirm(position: Long) {}
}
diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt
index cff57269f..c8fbde9b1 100644
--- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt
+++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt
@@ -52,6 +52,10 @@ class PlaybackStateManager {
}
private var mShuffleSeed = -1L
+ val song: Song? get() = mSong
+ val position: Long get() = mPosition
+ val queue: MutableList get() = mQueue
+ val index: Int get() = mIndex
val isPlaying: Boolean get() = mIsPlaying
val isShuffling: Boolean get() = mIsShuffling
@@ -164,6 +168,12 @@ class PlaybackStateManager {
mPosition = position
}
+ fun seekTo(position: Long) {
+ mPosition = position
+
+ callbacks.forEach { it.onSeekConfirm(position) }
+ }
+
// --- QUEUE FUNCTIONS ---
fun next() {
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index f3006f204..0a0c72017 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -48,6 +48,8 @@
Turn shuffle on
Turn shuffle off
The music playback service for Auxio
+ Music Playback
+ Auxio is playing music
Unknown Genre