diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt index e99ce4f98..830328140 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt @@ -105,9 +105,13 @@ class MainFragment : Fragment() { } } - // Start the playback service. - Intent(requireContext(), PlaybackService::class.java).also { - requireContext().startService(it) + // 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.") diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt index 5b398cee1..429a26644 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt @@ -72,10 +72,16 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener { // --- VIEWMODEL SETUP -- playbackModel.song.observe(viewLifecycleOwner) { - Log.d(this::class.simpleName, "Updating song display to ${it.name}.") + if (it != null) { + Log.d(this::class.simpleName, "Updating song display to ${it.name}.") - binding.song = it - binding.playbackSeekBar.max = it.seconds.toInt() + binding.song = it + binding.playbackSeekBar.max = it.seconds.toInt() + } else { + Log.d(this::class.simpleName, "No song played anymore, leaving.") + + findNavController().navigateUp() + } } playbackModel.index.observe(viewLifecycleOwner) { 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 9e561215e..da202eb1a 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackService.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackService.kt @@ -39,6 +39,7 @@ 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 { + // TODO: Use the ExoPlayer queue functionality [To an extent]? Could make things faster. private val player: SimpleExoPlayer by lazy { val p = SimpleExoPlayer.Builder(applicationContext).build() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { @@ -50,13 +51,6 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback { private val playbackManager = PlaybackStateManager.getInstance() private lateinit var mediaSession: MediaSessionCompat - private val buttonMediaCallback = object : MediaSessionCompat.Callback() { - override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean { - Log.d(this::class.simpleName, "Hello?") - - return true - } - } private val serviceJob = Job() private val serviceScope = CoroutineScope( @@ -67,19 +61,20 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback { private lateinit var notification: Notification + // --- SERVICE OVERRIDES --- + 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 onBind(intent: Intent): IBinder? = null override fun onCreate() { super.onCreate() + // Set up the media button callbacks mediaSession = MediaSessionCompat(this, packageName).apply { isActive = true } @@ -87,30 +82,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback { val connector = MediaSessionConnector(mediaSession) connector.setPlayer(player) connector.setMediaButtonEventHandler { _, _, mediaButtonEvent -> - val item = mediaButtonEvent - .getParcelableExtra(Intent.EXTRA_KEY_EVENT) as KeyEvent - - if (item.action == KeyEvent.ACTION_DOWN) { - when (item.keyCode) { - KeyEvent.KEYCODE_MEDIA_PAUSE, KeyEvent.KEYCODE_MEDIA_PLAY, - KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_HEADSETHOOK -> { - playbackManager.setPlayingStatus(!playbackManager.isPlaying) - } - - KeyEvent.KEYCODE_MEDIA_NEXT -> { - playbackManager.next() - } - - KeyEvent.KEYCODE_MEDIA_PREVIOUS -> { - playbackManager.prev() - } - - // TODO: Implement the other callbacks for - // CLOSE/STOP & REWIND - } - } - - true + handleMediaButtonEvent(mediaButtonEvent) } notification = createNotification() @@ -121,14 +93,19 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback { override fun onDestroy() { super.onDestroy() + stopForeground(true) + + // Release everything that could cause a memory leak if left around player.release() mediaSession.release() serviceJob.cancel() playbackManager.removeCallback(this) - stopForeground(true) + Log.d(this::class.simpleName, "Service destroyed.") } + // --- PLAYER EVENT LISTENER OVERRIDES --- + override fun onPlaybackStateChanged(state: Int) { if (state == Player.STATE_ENDED) { playbackManager.next() @@ -137,28 +114,31 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback { } } + // --- PLAYBACK STATE CALLBACK OVERRIDES --- + override fun onSongUpdate(song: Song?) { song?.let { - if (!isForeground) { - startForeground(NOTIF_ID, notification) - - isForeground = true - } - val item = MediaItem.fromUri(it.id.toURI()) player.setMediaItem(item) player.prepare() player.play() + + return } + + player.stop() } override fun onPlayingUpdate(isPlaying: Boolean) { - if (isPlaying) { + if (isPlaying && !player.isPlaying) { player.play() + startForeground(NOTIF_ID, notification) startPollingPosition() } else { player.pause() + + stopForeground(false) } } @@ -166,25 +146,63 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback { player.seekTo(position * 1000) } + // --- OTHER FUNCTIONS --- + // Awful Hack to get position polling to work, as exoplayer does not provide any // onPositionChanged callback for some inane reason. - // FIXME: Don't be surprised if this causes problems. - + // FIXME: There has to be a better way of polling positions. private fun pollCurrentPosition() = flow { - while (player.currentPosition <= player.duration) { + while (player.isPlaying) { emit(player.currentPosition) - delay(500) + delay(250) } }.conflate() private fun startPollingPosition() { serviceScope.launch { - pollCurrentPosition().takeWhile { true }.collect { + pollCurrentPosition().takeWhile { player.isPlaying }.collect { playbackManager.setPosition(it / 1000) } } } + // Handle a media button event. + private fun handleMediaButtonEvent(event: Intent): Boolean { + val item = event + .getParcelableExtra(Intent.EXTRA_KEY_EVENT) as KeyEvent + + if (item.action == KeyEvent.ACTION_DOWN) { + return when (item.keyCode) { + KeyEvent.KEYCODE_MEDIA_PAUSE, KeyEvent.KEYCODE_MEDIA_PLAY, + KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_HEADSETHOOK -> { + playbackManager.setPlayingStatus(!playbackManager.isPlaying) + + true + } + + KeyEvent.KEYCODE_MEDIA_NEXT -> { + playbackManager.next() + + true + } + + KeyEvent.KEYCODE_MEDIA_PREVIOUS -> { + playbackManager.prev() + + true + } + + // TODO: Implement the other callbacks for + // CLOSE/STOP & REWIND + else -> 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 @@ -197,7 +215,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback { ) notificationManager.createNotificationChannel(channel) } - // TODO: Placeholder, implement proper media controls :) + // TODO: Placeholder, implement proper media controls. val notif = NotificationCompat.Builder( applicationContext, CHANNEL_ID 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 a6cbccd05..2b0488add 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -18,10 +18,10 @@ import org.oxycblt.auxio.playback.state.PlaybackStateManager // TODO: Implement Looping Modes // TODO: Implement User Queue // TODO: Implement Persistence through Bundles/Databases/Idk -class PlaybackViewModel() : ViewModel(), PlaybackStateCallback { +class PlaybackViewModel : ViewModel(), PlaybackStateCallback { // Playback - private val mSong = MutableLiveData() - val song: LiveData get() = mSong + private val mSong = MutableLiveData() + val song: LiveData get() = mSong private val mPosition = MutableLiveData(0L) val position: LiveData get() = mPosition @@ -44,6 +44,9 @@ class PlaybackViewModel() : ViewModel(), PlaybackStateCallback { private val mIsSeeking = MutableLiveData(false) val isSeeking: LiveData get() = mIsSeeking + private var mServiceStarted = false + val serviceStarted: Boolean get() = mServiceStarted + val formattedPosition = Transformations.map(mPosition) { it.toDuration() } @@ -179,6 +182,10 @@ class PlaybackViewModel() : ViewModel(), PlaybackStateCallback { mIsSeeking.value = value } + fun setServiceStatus(value: Boolean) { + mServiceStarted = value + } + // --- OVERRIDES --- override fun onCleared() { @@ -186,9 +193,7 @@ class PlaybackViewModel() : ViewModel(), PlaybackStateCallback { } override fun onSongUpdate(song: Song?) { - song?.let { - mSong.value = it - } + mSong.value = song } override fun onPositionUpdate(position: Long) { 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 e2272741b..9fdb2f2fd 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,5 +9,7 @@ interface PlaybackStateCallback { fun onPlayingUpdate(isPlaying: Boolean) {} fun onShuffleUpdate(isShuffling: Boolean) {} fun onIndexUpdate(index: Int) {} + + // Service callbacks 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 c8fbde9b1..08faa7b2d 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 @@ -165,7 +165,13 @@ class PlaybackStateManager { } fun setPosition(position: Long) { - mPosition = position + // Due to the hacky way I poll ExoPlayer positions, don't accept any bugged positions + // that are over the duration of the song. + mSong?.let { + if (position <= it.seconds) { + mPosition = position + } + } } fun seekTo(position: Long) { @@ -177,8 +183,14 @@ class PlaybackStateManager { // --- QUEUE FUNCTIONS --- fun next() { - if (mIndex < mQueue.size) { + if (mIndex < mQueue.lastIndex) { mIndex = mIndex.inc() + } else { + // TODO: Implement option so that the playlist loops instead of stops + mQueue = mutableListOf() + mSong = null + + return } updatePlayback(mQueue[mIndex])