Make PlaybackService persistent
Make PlaybackService solely rely on PlaybackStateManager, allowing the service to run beyond the main Auxio activity.
This commit is contained in:
parent
ac5e6ba140
commit
85475b5c61
6 changed files with 96 additions and 34 deletions
|
@ -26,7 +26,6 @@
|
|||
<service android:name=".playback.PlaybackService"
|
||||
android:icon="@drawable/ic_launcher_foreground"
|
||||
android:description="@string/description_service_playback"
|
||||
android:stopWithTask="false"/>
|
||||
android:stopWithTask="false" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
|
|
|
@ -9,4 +9,5 @@ interface PlaybackStateCallback {
|
|||
fun onPlayingUpdate(isPlaying: Boolean) {}
|
||||
fun onShuffleUpdate(isShuffling: Boolean) {}
|
||||
fun onIndexUpdate(index: Int) {}
|
||||
fun onSeekConfirm(position: Long) {}
|
||||
}
|
||||
|
|
|
@ -52,6 +52,10 @@ class PlaybackStateManager {
|
|||
}
|
||||
private var mShuffleSeed = -1L
|
||||
|
||||
val song: Song? get() = mSong
|
||||
val position: Long get() = mPosition
|
||||
val queue: MutableList<Song> 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() {
|
||||
|
|
|
@ -48,6 +48,8 @@
|
|||
<string name="description_shuffle_on">Turn shuffle on</string>
|
||||
<string name="description_shuffle_off">Turn shuffle off</string>
|
||||
<string name="description_service_playback">The music playback service for Auxio</string>
|
||||
<string name="description_notif_playback">Music Playback</string>
|
||||
<string name="description_playback">Auxio is playing music</string>
|
||||
|
||||
<!-- Placeholder Namespace | Placeholder values -->
|
||||
<string name="placeholder_genre">Unknown Genre</string>
|
||||
|
|
Loading…
Reference in a new issue