Add Playback Notification
Add a MediaStyle Notification for music playback.
This commit is contained in:
parent
a4f55873ec
commit
e00930cc5f
9 changed files with 204 additions and 96 deletions
|
@ -1,9 +1,11 @@
|
||||||
package org.oxycblt.auxio
|
package org.oxycblt.auxio
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import org.oxycblt.auxio.playback.PlaybackService
|
||||||
import org.oxycblt.auxio.theme.accent
|
import org.oxycblt.auxio.theme.accent
|
||||||
|
|
||||||
// FIXME: Fix bug where fast navigation will break the animations and
|
// FIXME: Fix bug where fast navigation will break the animations and
|
||||||
|
@ -16,4 +18,12 @@ class MainActivity : AppCompatActivity(R.layout.activity_main) {
|
||||||
|
|
||||||
return super.onCreateView(name, context, attrs)
|
return super.onCreateView(name, context, attrs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
|
||||||
|
Intent(this, PlaybackService::class.java).also {
|
||||||
|
this.startService(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package org.oxycblt.auxio
|
package org.oxycblt.auxio
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
@ -16,7 +15,6 @@ import com.google.android.material.tabs.TabLayoutMediator
|
||||||
import org.oxycblt.auxio.databinding.FragmentMainBinding
|
import org.oxycblt.auxio.databinding.FragmentMainBinding
|
||||||
import org.oxycblt.auxio.library.LibraryFragment
|
import org.oxycblt.auxio.library.LibraryFragment
|
||||||
import org.oxycblt.auxio.music.MusicStore
|
import org.oxycblt.auxio.music.MusicStore
|
||||||
import org.oxycblt.auxio.playback.PlaybackService
|
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.songs.SongsFragment
|
import org.oxycblt.auxio.songs.SongsFragment
|
||||||
import org.oxycblt.auxio.theme.accent
|
import org.oxycblt.auxio.theme.accent
|
||||||
|
@ -105,15 +103,6 @@ class MainFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start the playback service [If not already]
|
|
||||||
if (!playbackModel.serviceStarted) {
|
|
||||||
Intent(requireContext(), PlaybackService::class.java).also {
|
|
||||||
requireContext().startService(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
playbackModel.setServiceStatus(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.d(this::class.simpleName, "Fragment Created.")
|
Log.d(this::class.simpleName, "Fragment Created.")
|
||||||
|
|
||||||
return binding.root
|
return binding.root
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
package org.oxycblt.auxio.music.coil
|
package org.oxycblt.auxio.music.coil
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
import androidx.databinding.BindingAdapter
|
import androidx.databinding.BindingAdapter
|
||||||
import coil.Coil
|
import coil.Coil
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
|
@ -129,6 +131,17 @@ fun ImageView.bindGenreImage(genre: Genre) {
|
||||||
Coil.imageLoader(context).enqueue(request)
|
Coil.imageLoader(context).enqueue(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get a bitmap for a song, onDone will be called when the bitmap is loaded.
|
||||||
|
fun getBitmap(song: Song, context: Context, onDone: (Bitmap) -> Unit) {
|
||||||
|
Coil.enqueue(
|
||||||
|
ImageRequest.Builder(context)
|
||||||
|
.data(song.album.coverUri)
|
||||||
|
.error(R.drawable.ic_song)
|
||||||
|
.target { onDone(it.toBitmap()) }
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Get the base request used across the other functions.
|
// Get the base request used across the other functions.
|
||||||
private fun getDefaultRequest(context: Context, imageView: ImageView): ImageRequest.Builder {
|
private fun getDefaultRequest(context: Context, imageView: ImageView): ImageRequest.Builder {
|
||||||
return ImageRequest.Builder(context)
|
return ImageRequest.Builder(context)
|
||||||
|
|
|
@ -138,6 +138,8 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
|
||||||
binding.playbackLoop.imageTintList = accentColor
|
binding.playbackLoop.imageTintList = accentColor
|
||||||
binding.playbackLoop.setImageResource(R.drawable.ic_loop)
|
binding.playbackLoop.setImageResource(R.drawable.ic_loop)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
else -> return@observe
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
package org.oxycblt.auxio.playback
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.content.Context
|
||||||
|
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.R
|
||||||
|
import org.oxycblt.auxio.music.Song
|
||||||
|
import org.oxycblt.auxio.music.coil.getBitmap
|
||||||
|
|
||||||
|
internal class PlaybackNotificationHolder {
|
||||||
|
private lateinit var mNotification: Notification
|
||||||
|
|
||||||
|
private lateinit var notificationManager: NotificationManager
|
||||||
|
private lateinit var baseNotification: NotificationCompat.Builder
|
||||||
|
|
||||||
|
fun init(context: Context, session: MediaSessionCompat) {
|
||||||
|
// 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_notif_playback),
|
||||||
|
NotificationManager.IMPORTANCE_DEFAULT
|
||||||
|
)
|
||||||
|
notificationManager.createNotificationChannel(channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
baseNotification = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||||
|
.setSmallIcon(R.drawable.ic_song)
|
||||||
|
.setStyle(MediaStyle().setMediaSession(session.sessionToken))
|
||||||
|
.setChannelId(CHANNEL_ID)
|
||||||
|
.setShowWhen(false)
|
||||||
|
.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. After 6 years.
|
||||||
|
baseNotification
|
||||||
|
.setContentTitle(song.name)
|
||||||
|
.setContentText(
|
||||||
|
playbackService.getString(
|
||||||
|
R.string.format_info,
|
||||||
|
song.album.artist.name,
|
||||||
|
song.album.name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
getBitmap(song, playbackService) {
|
||||||
|
baseNotification.setLargeIcon(it)
|
||||||
|
mNotification = baseNotification.build()
|
||||||
|
playbackService.startForeground(NOTIFICATION_ID, mNotification)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val CHANNEL_ID = "CHANNEL_AUXIO_PLAYBACK"
|
||||||
|
const val NOTIFICATION_ID = 0xA0A0
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,5 @@
|
||||||
package org.oxycblt.auxio.playback
|
package org.oxycblt.auxio.playback
|
||||||
|
|
||||||
import android.app.Notification
|
|
||||||
import android.app.NotificationChannel
|
|
||||||
import android.app.NotificationManager
|
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.bluetooth.BluetoothDevice
|
import android.bluetooth.BluetoothDevice
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
|
@ -13,11 +10,12 @@ import android.media.AudioManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import android.support.v4.media.MediaMetadataCompat
|
||||||
import android.support.v4.media.session.MediaSessionCompat
|
import android.support.v4.media.session.MediaSessionCompat
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
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.MediaItem
|
import com.google.android.exoplayer2.MediaItem
|
||||||
import com.google.android.exoplayer2.Player
|
import com.google.android.exoplayer2.Player
|
||||||
import com.google.android.exoplayer2.SimpleExoPlayer
|
import com.google.android.exoplayer2.SimpleExoPlayer
|
||||||
|
@ -32,49 +30,38 @@ import kotlinx.coroutines.flow.conflate
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.takeWhile
|
import kotlinx.coroutines.flow.takeWhile
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.oxycblt.auxio.R
|
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
import org.oxycblt.auxio.music.coil.getBitmap
|
||||||
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.PlaybackStateCallback
|
import org.oxycblt.auxio.playback.state.PlaybackStateCallback
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||||
|
|
||||||
private const val CHANNEL_ID = "CHANNEL_AUXIO_PLAYBACK"
|
|
||||||
private const val NOTIF_ID = 0xA0A0
|
|
||||||
private const val CONNECTED = 1
|
|
||||||
private const val DISCONNECTED = 0
|
|
||||||
|
|
||||||
// A Service that manages the single ExoPlayer instance and manages the system-side
|
// A Service that manages the single ExoPlayer instance and manages the system-side
|
||||||
// aspects of playback.
|
// aspects of playback.
|
||||||
class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
|
class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
|
||||||
// TODO: Use the ExoPlayer queue functionality [To an extent]? Could make things faster.
|
|
||||||
private val player: SimpleExoPlayer by lazy {
|
private val player: SimpleExoPlayer by lazy {
|
||||||
val p = SimpleExoPlayer.Builder(applicationContext).build()
|
SimpleExoPlayer.Builder(applicationContext).build()
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
||||||
p.experimentalSetOffloadSchedulingEnabled(true)
|
|
||||||
}
|
|
||||||
p.addListener(this)
|
|
||||||
p
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val playbackManager = PlaybackStateManager.getInstance()
|
private val playbackManager = PlaybackStateManager.getInstance()
|
||||||
private lateinit var mediaSession: MediaSessionCompat
|
private lateinit var mediaSession: MediaSessionCompat
|
||||||
private lateinit var systemReceiver: SystemEventReceiver
|
private lateinit var systemReceiver: SystemEventReceiver
|
||||||
private var changeIsFromSystem = false
|
private val notificationHolder = PlaybackNotificationHolder()
|
||||||
|
|
||||||
|
private var changeIsFromAudioFocus = true
|
||||||
|
|
||||||
private val serviceJob = Job()
|
private val serviceJob = Job()
|
||||||
private val serviceScope = CoroutineScope(
|
private val serviceScope = CoroutineScope(
|
||||||
serviceJob + Dispatchers.Main
|
serviceJob + Dispatchers.Main
|
||||||
)
|
)
|
||||||
|
|
||||||
private lateinit var notification: Notification
|
|
||||||
|
|
||||||
// --- SERVICE OVERRIDES ---
|
// --- SERVICE OVERRIDES ---
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
Log.d(this::class.simpleName, "Service is active.")
|
Log.d(this::class.simpleName, "Service is active.")
|
||||||
|
|
||||||
return START_STICKY
|
return START_NOT_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBind(intent: Intent): IBinder? = null
|
override fun onBind(intent: Intent): IBinder? = null
|
||||||
|
@ -82,16 +69,8 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
// Set up the media button callbacks
|
// --- PLAYER SETUP ---
|
||||||
mediaSession = MediaSessionCompat(this, packageName).apply {
|
player.addListener(this)
|
||||||
isActive = true
|
|
||||||
}
|
|
||||||
|
|
||||||
val connector = MediaSessionConnector(mediaSession)
|
|
||||||
connector.setPlayer(player)
|
|
||||||
connector.setMediaButtonEventHandler { _, _, mediaButtonEvent ->
|
|
||||||
handleMediaButtonEvent(mediaButtonEvent)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up AudioFocus/AudioAttributes
|
// Set up AudioFocus/AudioAttributes
|
||||||
player.setAudioAttributes(
|
player.setAudioAttributes(
|
||||||
|
@ -102,7 +81,31 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
|
|
||||||
notification = createNotification()
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
player.experimentalSetOffloadSchedulingEnabled(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- PLAYBACKSTATEMANAGER SETUP ---
|
||||||
|
|
||||||
|
playbackManager.addCallback(this)
|
||||||
|
|
||||||
|
if (playbackManager.song != null) {
|
||||||
|
restorePlayer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- SYSTEM RECEIVER SETUP ---
|
||||||
|
|
||||||
|
// Set up the media button callbacks
|
||||||
|
mediaSession = MediaSessionCompat(this, packageName).apply {
|
||||||
|
isActive = true
|
||||||
|
|
||||||
|
MediaSessionConnector(this).apply {
|
||||||
|
setPlayer(player)
|
||||||
|
setMediaButtonEventHandler { _, _, mediaButtonEvent ->
|
||||||
|
handleMediaButtonEvent(mediaButtonEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Set up callback for system events
|
// Set up callback for system events
|
||||||
systemReceiver = SystemEventReceiver()
|
systemReceiver = SystemEventReceiver()
|
||||||
|
@ -115,7 +118,9 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
|
||||||
registerReceiver(systemReceiver, this)
|
registerReceiver(systemReceiver, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
playbackManager.addCallback(this)
|
// --- NOTIFICATION SETUP ---
|
||||||
|
|
||||||
|
notificationHolder.init(applicationContext, mediaSession)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
@ -136,6 +141,8 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
|
||||||
// --- PLAYER EVENT LISTENER OVERRIDES ---
|
// --- PLAYER EVENT LISTENER OVERRIDES ---
|
||||||
|
|
||||||
override fun onPlaybackStateChanged(state: Int) {
|
override fun onPlaybackStateChanged(state: Int) {
|
||||||
|
changeIsFromAudioFocus = false
|
||||||
|
|
||||||
if (state == Player.STATE_ENDED) {
|
if (state == Player.STATE_ENDED) {
|
||||||
playbackManager.next()
|
playbackManager.next()
|
||||||
} else if (state == Player.STATE_READY) {
|
} else if (state == Player.STATE_READY) {
|
||||||
|
@ -144,13 +151,11 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||||
// If the change to playing occurred from the system instead of PlaybackStateManager, then
|
// Only sync the playing status with PlaybackStateManager if the change occurred
|
||||||
// sync the playing value to PlaybackStateManager to keep it up ton date.
|
// from an Audio Focus change. Nowhere else.
|
||||||
if (isPlaying != playbackManager.isPlaying && changeIsFromSystem) {
|
if (isPlaying != playbackManager.isPlaying && changeIsFromAudioFocus) {
|
||||||
playbackManager.setPlayingStatus(isPlaying)
|
playbackManager.setPlayingStatus(isPlaying)
|
||||||
}
|
}
|
||||||
|
|
||||||
changeIsFromSystem = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||||
|
@ -162,30 +167,46 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPlayerError(error: ExoPlaybackException) {
|
||||||
|
// If there's any issue, just go to the next song.
|
||||||
|
playbackManager.next()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPositionDiscontinuity(reason: Int) {
|
||||||
|
if (reason == Player.DISCONTINUITY_REASON_SEEK) {
|
||||||
|
playbackManager.setPosition(player.currentPosition / 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- PLAYBACK STATE CALLBACK OVERRIDES ---
|
// --- PLAYBACK STATE CALLBACK OVERRIDES ---
|
||||||
|
|
||||||
override fun onSongUpdate(song: Song?) {
|
override fun onSongUpdate(song: Song?) {
|
||||||
changeIsFromSystem = false
|
changeIsFromAudioFocus = false
|
||||||
|
|
||||||
song?.let {
|
song?.let {
|
||||||
val item = MediaItem.fromUri(it.id.toURI())
|
val item = MediaItem.fromUri(it.id.toURI())
|
||||||
|
|
||||||
player.setMediaItem(item)
|
player.setMediaItem(item)
|
||||||
player.prepare()
|
player.prepare()
|
||||||
player.play()
|
player.play()
|
||||||
|
|
||||||
|
uploadMetadataToSession(it)
|
||||||
|
notificationHolder.setMetadata(playbackManager.song!!, this)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stop playing/the notification if there's nothing to play.
|
||||||
player.stop()
|
player.stop()
|
||||||
|
stopForeground(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPlayingUpdate(isPlaying: Boolean) {
|
override fun onPlayingUpdate(isPlaying: Boolean) {
|
||||||
changeIsFromSystem = false
|
changeIsFromAudioFocus = false
|
||||||
|
|
||||||
if (isPlaying && !player.isPlaying) {
|
if (isPlaying && !player.isPlaying) {
|
||||||
player.play()
|
player.play()
|
||||||
|
|
||||||
startForeground(NOTIF_ID, notification)
|
|
||||||
startPollingPosition()
|
startPollingPosition()
|
||||||
} else {
|
} else {
|
||||||
player.pause()
|
player.pause()
|
||||||
|
@ -196,7 +217,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLoopUpdate(mode: LoopMode) {
|
override fun onLoopUpdate(mode: LoopMode) {
|
||||||
changeIsFromSystem = false
|
changeIsFromAudioFocus = false
|
||||||
|
|
||||||
when (mode) {
|
when (mode) {
|
||||||
LoopMode.NONE -> {
|
LoopMode.NONE -> {
|
||||||
|
@ -209,16 +230,44 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSeekConfirm(position: Long) {
|
override fun onSeekConfirm(position: Long) {
|
||||||
changeIsFromSystem = false
|
changeIsFromAudioFocus = false
|
||||||
|
|
||||||
player.seekTo(position * 1000)
|
player.seekTo(position * 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- OTHER FUNCTIONS ---
|
// --- OTHER FUNCTIONS ---
|
||||||
|
|
||||||
|
private fun restorePlayer() {
|
||||||
|
playbackManager.song?.let {
|
||||||
|
val item = MediaItem.fromUri(it.id.toURI())
|
||||||
|
player.setMediaItem(item)
|
||||||
|
player.prepare()
|
||||||
|
player.play()
|
||||||
|
|
||||||
|
notificationHolder.setMetadata(it, this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun uploadMetadataToSession(song: Song) {
|
||||||
|
val builder = MediaMetadataCompat.Builder()
|
||||||
|
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, song.name)
|
||||||
|
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, song.name)
|
||||||
|
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, song.album.artist.name)
|
||||||
|
.putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, song.album.artist.name)
|
||||||
|
.putString(MediaMetadataCompat.METADATA_KEY_COMPOSER, song.album.artist.name)
|
||||||
|
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, song.album.artist.name)
|
||||||
|
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, song.album.name)
|
||||||
|
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.duration)
|
||||||
|
|
||||||
|
getBitmap(song, this) {
|
||||||
|
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, it)
|
||||||
|
mediaSession.setMetadata(builder.build())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Awful Hack to get position polling to work, as exoplayer does not provide any
|
// Awful Hack to get position polling to work, as exoplayer does not provide any
|
||||||
// onPositionChanged callback for some inane reason.
|
// onPositionChanged callback for some inane reason.
|
||||||
// FIXME: There has to be a better way of polling positions.
|
// TODO: MediaSession might have a callback for positions. Idk.
|
||||||
private fun pollCurrentPosition() = flow {
|
private fun pollCurrentPosition() = flow {
|
||||||
while (player.isPlaying) {
|
while (player.isPlaying) {
|
||||||
emit(player.currentPosition)
|
emit(player.currentPosition)
|
||||||
|
@ -274,35 +323,6 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a notification
|
|
||||||
// TODO: Spin this off into its own object!
|
|
||||||
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.label_notif_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.label_is_playing))
|
|
||||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
|
||||||
.setChannelId(CHANNEL_ID)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
return notif
|
|
||||||
}
|
|
||||||
|
|
||||||
// BroadcastReceiver for receiving system events [E.G Headphones connected/disconnected]
|
// BroadcastReceiver for receiving system events [E.G Headphones connected/disconnected]
|
||||||
private inner class SystemEventReceiver : BroadcastReceiver() {
|
private inner class SystemEventReceiver : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
@ -348,4 +368,9 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val DISCONNECTED = 0
|
||||||
|
private const val CONNECTED = 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,9 +48,6 @@ class PlaybackViewModel : ViewModel(), PlaybackStateCallback {
|
||||||
private val mIsSeeking = MutableLiveData(false)
|
private val mIsSeeking = MutableLiveData(false)
|
||||||
val isSeeking: LiveData<Boolean> get() = mIsSeeking
|
val isSeeking: LiveData<Boolean> get() = mIsSeeking
|
||||||
|
|
||||||
private var mServiceStarted = false
|
|
||||||
val serviceStarted: Boolean get() = mServiceStarted
|
|
||||||
|
|
||||||
val formattedPosition = Transformations.map(mPosition) {
|
val formattedPosition = Transformations.map(mPosition) {
|
||||||
it.toDuration()
|
it.toDuration()
|
||||||
}
|
}
|
||||||
|
@ -190,10 +187,6 @@ class PlaybackViewModel : ViewModel(), PlaybackStateCallback {
|
||||||
mIsSeeking.value = value
|
mIsSeeking.value = value
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setServiceStatus(value: Boolean) {
|
|
||||||
mServiceStarted = value
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- OVERRIDES ---
|
// --- OVERRIDES ---
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
|
|
|
@ -11,17 +11,19 @@ import org.oxycblt.auxio.music.Song
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
// The manager of the current playback state [Current Song, Queue, Shuffling]
|
// The manager of the current playback state [Current Song, Queue, Shuffling]
|
||||||
// Never use this for ANYTHING UI related, that's what PlaybackViewModel is for.
|
// This class is for sole use by the classes in /playback/.
|
||||||
// Yes, I know MediaSessionCompat and friends exist, but I like having full control over the
|
// If you want to add system-side things, add to PlaybackService.
|
||||||
// playback state instead of dealing with android's likely buggy code.
|
// If you want to add ui-side things, add to PlaybackViewModel.
|
||||||
class PlaybackStateManager {
|
// [Yes, I know MediaSessionCompat exists, but I like having full control over the
|
||||||
|
// playback state instead of dealing with android's likely buggy code.]
|
||||||
|
internal class PlaybackStateManager {
|
||||||
// Playback
|
// Playback
|
||||||
private var mSong: Song? = null
|
private var mSong: Song? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
callbacks.forEach { it.onSongUpdate(value) }
|
callbacks.forEach { it.onSongUpdate(value) }
|
||||||
}
|
}
|
||||||
private var mPosition: Long = 0
|
private var mPosition: Long = 0 // TODO: Consider using millis instead of seconds?
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
callbacks.forEach { it.onPositionUpdate(value) }
|
callbacks.forEach { it.onPositionUpdate(value) }
|
||||||
|
|
|
@ -56,6 +56,7 @@
|
||||||
<string name="placeholder_genre">Unknown Genre</string>
|
<string name="placeholder_genre">Unknown Genre</string>
|
||||||
<string name="placeholder_artist">Unknown Artist</string>
|
<string name="placeholder_artist">Unknown Artist</string>
|
||||||
<string name="placeholder_album">Unknown Album</string>
|
<string name="placeholder_album">Unknown Album</string>
|
||||||
|
<string name="placeholder_song">Unknown Song</string>
|
||||||
<string name="placeholder_no_date">No Date</string>
|
<string name="placeholder_no_date">No Date</string>
|
||||||
|
|
||||||
<!-- Format Namespace | Value formatting/plurals -->
|
<!-- Format Namespace | Value formatting/plurals -->
|
||||||
|
|
Loading…
Reference in a new issue