ultrasonic-app-subsonic-and.../ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaPlayerService.kt

801 lines
28 KiB
Kotlin
Raw Normal View History

2021-04-22 19:47:06 +02:00
/*
* MediaPlayerService.kt
* Copyright (C) 2009-2021 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
2021-04-22 19:25:50 +02:00
package org.moire.ultrasonic.service
2021-07-18 13:17:29 +02:00
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
2021-04-22 19:25:50 +02:00
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Handler
2021-07-04 22:42:18 +02:00
import android.os.IBinder
import android.os.Looper
2021-04-22 19:25:50 +02:00
import android.support.v4.media.session.MediaSessionCompat
import android.view.KeyEvent
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
2021-05-29 02:30:36 +02:00
import kotlin.collections.ArrayList
2021-04-22 19:47:06 +02:00
import org.koin.android.ext.android.inject
2021-04-22 19:25:50 +02:00
import org.moire.ultrasonic.R
import org.moire.ultrasonic.activity.NavigationActivity
import org.moire.ultrasonic.app.UApp
2021-04-22 19:25:50 +02:00
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.domain.PlayerState
import org.moire.ultrasonic.domain.RepeatMode
2021-06-07 13:17:00 +02:00
import org.moire.ultrasonic.imageloader.BitmapUtils
2021-04-22 19:25:50 +02:00
import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X1
import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X2
import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X3
import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider4X4
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.MediaSessionEventDistributor
import org.moire.ultrasonic.util.MediaSessionEventListener
import org.moire.ultrasonic.util.MediaSessionHandler
import org.moire.ultrasonic.util.NowPlayingEventDistributor
import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.ShufflePlayBuffer
import org.moire.ultrasonic.util.SimpleServiceBinder
import org.moire.ultrasonic.util.Util
2021-04-22 19:25:50 +02:00
import timber.log.Timber
2020-06-21 09:31:38 +02:00
/**
* Android Foreground Service for playing music
* while the rest of the Ultrasonic App is in the background.
2021-08-30 10:08:27 +02:00
*
* "A foreground service is a service that the user is
* actively aware of and isnt a candidate for the system to kill when low on memory."
*/
@Suppress("LargeClass")
2021-07-04 22:42:18 +02:00
class MediaPlayerService : Service() {
private val binder: IBinder = SimpleServiceBinder(this)
2021-04-22 19:25:50 +02:00
private val scrobbler = Scrobbler()
2021-04-22 20:17:03 +02:00
2021-04-22 19:47:06 +02:00
private val jukeboxMediaPlayer by inject<JukeboxMediaPlayer>()
private val downloadQueueSerializer by inject<DownloadQueueSerializer>()
private val shufflePlayBuffer by inject<ShufflePlayBuffer>()
private val downloader by inject<Downloader>()
private val localMediaPlayer by inject<LocalMediaPlayer>()
private val nowPlayingEventDistributor by inject<NowPlayingEventDistributor>()
private val mediaSessionEventDistributor by inject<MediaSessionEventDistributor>()
private val mediaSessionHandler by inject<MediaSessionHandler>()
2021-04-22 19:47:06 +02:00
2021-04-22 19:25:50 +02:00
private var mediaSession: MediaSessionCompat? = null
private var mediaSessionToken: MediaSessionCompat.Token? = null
2021-04-22 19:25:50 +02:00
private var isInForeground = false
private var notificationBuilder: NotificationCompat.Builder? = null
private lateinit var mediaSessionEventListener: MediaSessionEventListener
2021-04-22 19:47:06 +02:00
2021-04-22 20:17:03 +02:00
private val repeatMode: RepeatMode
get() = Settings.repeatMode
2021-04-22 19:25:50 +02:00
2021-07-04 22:42:18 +02:00
override fun onBind(intent: Intent): IBinder {
return binder
}
2021-04-22 19:25:50 +02:00
override fun onCreate() {
super.onCreate()
2021-04-22 19:47:06 +02:00
shufflePlayBuffer.onCreate()
localMediaPlayer.init()
2021-04-22 19:25:50 +02:00
setupOnCurrentPlayingChangedHandler()
setupOnPlayerStateChangedHandler()
setupOnSongCompletedHandler()
2021-04-22 19:47:06 +02:00
localMediaPlayer.onPrepared = {
downloadQueueSerializer.serializeDownloadQueue(
2021-08-28 00:02:50 +02:00
downloader.playlist,
2021-04-22 19:47:06 +02:00
downloader.currentPlayingIndex,
playerPosition
2021-04-22 19:25:50 +02:00
)
null
}
2021-04-22 19:47:06 +02:00
localMediaPlayer.onNextSongRequested = Runnable { setNextPlaying() }
2021-07-18 13:17:29 +02:00
mediaSessionEventListener = object : MediaSessionEventListener {
override fun onMediaSessionTokenCreated(token: MediaSessionCompat.Token) {
mediaSessionToken = token
}
override fun onSkipToQueueItemRequested(id: Long) {
play(id.toInt())
}
}
mediaSessionEventDistributor.subscribe(mediaSessionEventListener)
mediaSessionHandler.initialize()
2020-06-21 09:31:38 +02:00
// Create Notification Channel
2021-04-22 19:25:50 +02:00
createNotificationChannel()
2020-06-21 09:31:38 +02:00
2021-04-22 19:47:06 +02:00
// Update notification early. It is better to show an empty one temporarily
// than waiting too long and letting Android kill the app
2021-04-22 19:25:50 +02:00
updateNotification(PlayerState.IDLE, null)
instance = this
Timber.i("MediaPlayerService created")
2020-06-21 09:31:38 +02:00
}
2021-04-22 19:25:50 +02:00
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
return START_NOT_STICKY
2020-06-21 09:31:38 +02:00
}
2021-04-22 19:25:50 +02:00
override fun onDestroy() {
super.onDestroy()
instance = null
2020-06-26 13:31:31 +02:00
try {
mediaSessionEventDistributor.unsubscribe(mediaSessionEventListener)
mediaSessionHandler.release()
2021-04-22 19:47:06 +02:00
localMediaPlayer.release()
downloader.stop()
shufflePlayBuffer.onDestroy()
mediaSession?.release()
2021-05-06 12:53:25 +02:00
mediaSession = null
2021-04-22 19:25:50 +02:00
} catch (ignored: Throwable) {
2020-06-21 09:31:38 +02:00
}
2021-04-22 19:25:50 +02:00
Timber.i("MediaPlayerService stopped")
2020-06-21 09:31:38 +02:00
}
2021-04-22 19:25:50 +02:00
private fun stopIfIdle() {
synchronized(instanceLock) {
2021-04-22 19:47:06 +02:00
// currentPlaying could be changed from another thread in the meantime,
// so check again before stopping for good
if (localMediaPlayer.currentPlaying == null ||
localMediaPlayer.playerState === PlayerState.STOPPED
) {
stopSelf()
}
2020-06-26 13:31:31 +02:00
}
}
fun notifyDownloaderStopped() {
// TODO It would be nice to know if the service really can be stopped instead of just
// checking if it is idle once...
val handler = Handler(Looper.getMainLooper())
handler.postDelayed({ stopIfIdle() }, 1000)
}
2021-04-22 19:25:50 +02:00
@Synchronized
fun seekTo(position: Int) {
2021-04-22 19:47:06 +02:00
if (jukeboxMediaPlayer.isEnabled) {
// TODO These APIs should be more aligned
val seconds = position / 1000
jukeboxMediaPlayer.skip(downloader.currentPlayingIndex, seconds)
2021-04-22 19:25:50 +02:00
} else {
2021-04-22 19:47:06 +02:00
localMediaPlayer.seekTo(position)
2020-06-21 09:31:38 +02:00
}
}
2021-04-22 19:25:50 +02:00
@get:Synchronized
val playerPosition: Int
get() {
2021-04-22 19:47:06 +02:00
if (localMediaPlayer.playerState === PlayerState.IDLE ||
localMediaPlayer.playerState === PlayerState.DOWNLOADING ||
localMediaPlayer.playerState === PlayerState.PREPARING
) {
2021-04-22 19:25:50 +02:00
return 0
}
2021-04-22 19:47:06 +02:00
return if (jukeboxMediaPlayer.isEnabled) {
jukeboxMediaPlayer.positionSeconds * 1000
} else {
localMediaPlayer.playerPosition
}
2020-06-21 09:31:38 +02:00
}
2021-04-22 19:25:50 +02:00
@get:Synchronized
val playerDuration: Int
2021-04-22 19:47:06 +02:00
get() = localMediaPlayer.playerDuration
2020-06-21 09:31:38 +02:00
2021-04-22 19:25:50 +02:00
@Synchronized
fun setCurrentPlaying(currentPlayingIndex: Int) {
try {
2021-08-28 00:02:50 +02:00
localMediaPlayer.setCurrentPlaying(downloader.playlist[currentPlayingIndex])
} catch (ignored: IndexOutOfBoundsException) {
2020-06-21 09:31:38 +02:00
}
}
2021-04-22 19:25:50 +02:00
@Synchronized
fun setNextPlaying() {
val gaplessPlayback = Settings.gaplessPlayback
2021-04-22 19:47:06 +02:00
2021-04-22 19:25:50 +02:00
if (!gaplessPlayback) {
2021-04-22 19:47:06 +02:00
localMediaPlayer.clearNextPlaying(true)
2021-04-22 19:25:50 +02:00
return
2020-06-21 09:31:38 +02:00
}
2021-04-22 19:47:06 +02:00
var index = downloader.currentPlayingIndex
2021-04-22 19:25:50 +02:00
if (index != -1) {
when (repeatMode) {
RepeatMode.OFF -> index += 1
2021-08-28 00:02:50 +02:00
RepeatMode.ALL -> index = (index + 1) % downloader.playlist.size
2021-04-22 19:25:50 +02:00
RepeatMode.SINGLE -> {
}
else -> {
}
2020-06-21 09:31:38 +02:00
}
}
2021-04-22 19:47:06 +02:00
localMediaPlayer.clearNextPlaying(false)
2021-08-28 00:02:50 +02:00
if (index < downloader.playlist.size && index != -1) {
localMediaPlayer.setNextPlaying(downloader.playlist[index])
2021-04-22 19:25:50 +02:00
} else {
2021-04-22 19:47:06 +02:00
localMediaPlayer.clearNextPlaying(true)
2020-06-21 09:31:38 +02:00
}
}
2021-04-22 19:25:50 +02:00
@Synchronized
fun togglePlayPause() {
2021-04-22 19:47:06 +02:00
if (localMediaPlayer.playerState === PlayerState.PAUSED ||
localMediaPlayer.playerState === PlayerState.COMPLETED ||
localMediaPlayer.playerState === PlayerState.STOPPED
) {
2021-04-22 19:25:50 +02:00
start()
2021-04-22 19:47:06 +02:00
} else if (localMediaPlayer.playerState === PlayerState.IDLE) {
2021-04-22 19:25:50 +02:00
play()
2021-04-22 19:47:06 +02:00
} else if (localMediaPlayer.playerState === PlayerState.STARTED) {
2021-04-22 19:25:50 +02:00
pause()
2020-06-21 09:31:38 +02:00
}
}
2021-04-22 19:25:50 +02:00
@Synchronized
fun resumeOrPlay() {
2021-04-22 19:47:06 +02:00
if (localMediaPlayer.playerState === PlayerState.PAUSED ||
localMediaPlayer.playerState === PlayerState.COMPLETED ||
localMediaPlayer.playerState === PlayerState.STOPPED
) {
2021-04-22 19:25:50 +02:00
start()
2021-04-22 19:47:06 +02:00
} else if (localMediaPlayer.playerState === PlayerState.IDLE) {
2021-04-22 19:25:50 +02:00
play()
}
}
2020-06-21 09:31:38 +02:00
/**
* Plays either the current song (resume) or the first/next one in queue.
*/
2021-04-22 19:25:50 +02:00
@Synchronized
fun play() {
2021-04-22 19:47:06 +02:00
val current = downloader.currentPlayingIndex
2021-04-22 19:25:50 +02:00
if (current == -1) {
play(0)
} else {
play(current)
2020-06-21 09:31:38 +02:00
}
}
2021-04-22 19:25:50 +02:00
@Synchronized
fun play(index: Int) {
play(index, true)
2020-06-21 09:31:38 +02:00
}
2021-04-22 19:25:50 +02:00
@Synchronized
fun play(index: Int, start: Boolean) {
Timber.v("play requested for %d", index)
2021-08-28 00:02:50 +02:00
if (index < 0 || index >= downloader.playlist.size) {
2021-04-22 19:25:50 +02:00
resetPlayback()
} else {
setCurrentPlaying(index)
if (start) {
2021-04-22 19:47:06 +02:00
if (jukeboxMediaPlayer.isEnabled) {
jukeboxMediaPlayer.skip(index, 0)
localMediaPlayer.setPlayerState(PlayerState.STARTED)
2021-04-22 19:25:50 +02:00
} else {
2021-08-28 00:02:50 +02:00
localMediaPlayer.play(downloader.playlist[index])
2020-06-21 09:31:38 +02:00
}
}
2021-04-22 19:47:06 +02:00
downloader.checkDownloads()
2021-04-22 19:25:50 +02:00
setNextPlaying()
2020-06-21 09:31:38 +02:00
}
}
2021-04-22 19:25:50 +02:00
@Synchronized
private fun resetPlayback() {
2021-04-22 19:47:06 +02:00
localMediaPlayer.reset()
localMediaPlayer.setCurrentPlaying(null)
downloadQueueSerializer.serializeDownloadQueue(
2021-08-28 00:02:50 +02:00
downloader.playlist,
2021-04-22 19:47:06 +02:00
downloader.currentPlayingIndex, playerPosition
)
2020-06-21 09:31:38 +02:00
}
2021-04-22 19:25:50 +02:00
@Synchronized
fun pause() {
2021-04-22 19:47:06 +02:00
if (localMediaPlayer.playerState === PlayerState.STARTED) {
if (jukeboxMediaPlayer.isEnabled) {
jukeboxMediaPlayer.stop()
2021-04-22 19:25:50 +02:00
} else {
2021-04-22 19:47:06 +02:00
localMediaPlayer.pause()
2020-06-23 18:40:44 +02:00
}
2021-04-22 19:47:06 +02:00
localMediaPlayer.setPlayerState(PlayerState.PAUSED)
2020-06-21 09:31:38 +02:00
}
2020-06-23 18:40:44 +02:00
}
2021-04-22 19:25:50 +02:00
@Synchronized
fun stop() {
2021-04-22 19:47:06 +02:00
if (localMediaPlayer.playerState === PlayerState.STARTED) {
if (jukeboxMediaPlayer.isEnabled) {
jukeboxMediaPlayer.stop()
2021-04-22 19:25:50 +02:00
} else {
2021-04-22 19:47:06 +02:00
localMediaPlayer.pause()
2020-06-23 18:40:44 +02:00
}
2020-06-21 09:31:38 +02:00
}
2021-04-22 19:47:06 +02:00
localMediaPlayer.setPlayerState(PlayerState.STOPPED)
2020-06-21 09:31:38 +02:00
}
2021-04-22 19:25:50 +02:00
@Synchronized
fun start() {
2021-04-22 19:47:06 +02:00
if (jukeboxMediaPlayer.isEnabled) {
jukeboxMediaPlayer.start()
2021-04-22 19:25:50 +02:00
} else {
2021-04-22 19:47:06 +02:00
localMediaPlayer.start()
2020-06-23 18:40:44 +02:00
}
2021-04-22 19:47:06 +02:00
localMediaPlayer.setPlayerState(PlayerState.STARTED)
2020-06-21 09:31:38 +02:00
}
2021-04-22 19:47:06 +02:00
private fun updateWidget(playerState: PlayerState, song: MusicDirectory.Entry?) {
val started = playerState === PlayerState.STARTED
val context = this@MediaPlayerService
UltrasonicAppWidgetProvider4X1.getInstance().notifyChange(context, song, started, false)
UltrasonicAppWidgetProvider4X2.getInstance().notifyChange(context, song, started, true)
UltrasonicAppWidgetProvider4X3.getInstance().notifyChange(context, song, started, false)
UltrasonicAppWidgetProvider4X4.getInstance().notifyChange(context, song, started, false)
}
2021-04-22 19:47:06 +02:00
private fun setupOnCurrentPlayingChangedHandler() {
localMediaPlayer.onCurrentPlayingChanged = { currentPlaying: DownloadFile? ->
if (currentPlaying != null) {
Util.broadcastNewTrackInfo(this@MediaPlayerService, currentPlaying.song)
Util.broadcastA2dpMetaDataChange(
this@MediaPlayerService, playerPosition, currentPlaying,
downloader.all.size, downloader.currentPlayingIndex + 1
2021-04-22 19:47:06 +02:00
)
} else {
Util.broadcastNewTrackInfo(this@MediaPlayerService, null)
Util.broadcastA2dpMetaDataChange(
this@MediaPlayerService, playerPosition, null,
downloader.all.size, downloader.currentPlayingIndex + 1
2021-04-22 19:47:06 +02:00
)
}
// Update widget
val playerState = localMediaPlayer.playerState
val song = currentPlaying?.song
updateWidget(playerState, song)
if (currentPlaying != null) {
updateNotification(localMediaPlayer.playerState, currentPlaying)
nowPlayingEventDistributor.raiseShowNowPlayingEvent()
} else {
nowPlayingEventDistributor.raiseHideNowPlayingEvent()
stopForeground(true)
isInForeground = false
stopIfIdle()
}
null
}
}
private fun setupOnPlayerStateChangedHandler() {
localMediaPlayer.onPlayerStateChanged = {
playerState: PlayerState,
currentPlaying: DownloadFile?
->
val context = this@MediaPlayerService
// Notify MediaSession
2021-07-18 13:17:29 +02:00
mediaSessionHandler.updateMediaSession(
currentPlaying,
downloader.currentPlayingIndex.toLong(),
playerState
)
2021-04-22 19:25:50 +02:00
if (playerState === PlayerState.PAUSED) {
2021-04-22 19:47:06 +02:00
downloadQueueSerializer.serializeDownloadQueue(
2021-08-28 00:02:50 +02:00
downloader.playlist, downloader.currentPlayingIndex, playerPosition
2021-04-22 19:47:06 +02:00
)
}
2021-04-22 19:47:06 +02:00
val showWhenPaused = playerState !== PlayerState.STOPPED &&
Settings.isNotificationAlwaysEnabled
2021-04-22 19:47:06 +02:00
2021-04-22 19:25:50 +02:00
val show = playerState === PlayerState.STARTED || showWhenPaused
val song = currentPlaying?.song
2021-04-22 19:47:06 +02:00
Util.broadcastPlaybackStatusChange(context, playerState)
Util.broadcastA2dpPlayStatusChange(
context, playerState, song,
2021-08-28 00:02:50 +02:00
downloader.playlist.size,
downloader.playlist.indexOf(currentPlaying) + 1, playerPosition
2021-04-22 19:47:06 +02:00
)
2020-06-21 09:31:38 +02:00
// Update widget
2021-04-22 19:47:06 +02:00
updateWidget(playerState, song)
if (show) {
// Only update notification if player state is one that will change the icon
2021-04-22 19:25:50 +02:00
if (playerState === PlayerState.STARTED || playerState === PlayerState.PAUSED) {
updateNotification(playerState, currentPlaying)
2021-04-22 19:47:06 +02:00
nowPlayingEventDistributor.raiseShowNowPlayingEvent()
2020-06-21 09:31:38 +02:00
}
} else {
2021-04-22 19:47:06 +02:00
nowPlayingEventDistributor.raiseHideNowPlayingEvent()
2021-04-22 19:25:50 +02:00
stopForeground(true)
isInForeground = false
stopIfIdle()
}
2021-04-22 19:25:50 +02:00
if (playerState === PlayerState.STARTED) {
scrobbler.scrobble(currentPlaying, false)
2021-04-22 19:25:50 +02:00
} else if (playerState === PlayerState.COMPLETED) {
scrobbler.scrobble(currentPlaying, true)
2020-06-23 18:40:44 +02:00
}
2021-04-22 19:25:50 +02:00
null
}
2020-06-21 09:31:38 +02:00
}
2021-04-22 19:25:50 +02:00
private fun setupOnSongCompletedHandler() {
2021-04-22 19:47:06 +02:00
localMediaPlayer.onSongCompleted = { currentPlaying: DownloadFile? ->
val index = downloader.currentPlayingIndex
if (currentPlaying != null) {
2021-04-22 19:25:50 +02:00
val song = currentPlaying.song
if (song.bookmarkPosition > 0 && Settings.shouldClearBookmark) {
val musicService = getMusicService()
try {
musicService.deleteBookmark(song.id)
2021-04-22 19:25:50 +02:00
} catch (ignored: Exception) {
2020-06-21 09:31:38 +02:00
}
2020-06-23 18:40:44 +02:00
}
}
if (index != -1) {
2021-04-22 19:25:50 +02:00
when (repeatMode) {
RepeatMode.OFF -> {
2021-08-28 00:02:50 +02:00
if (index + 1 < 0 || index + 1 >= downloader.playlist.size) {
if (Settings.shouldClearPlaylist) {
2021-04-22 19:25:50 +02:00
clear(true)
2021-04-22 19:47:06 +02:00
jukeboxMediaPlayer.updatePlaylist()
2020-06-21 09:31:38 +02:00
}
2021-04-22 19:25:50 +02:00
resetPlayback()
2021-04-22 19:47:06 +02:00
} else {
play(index + 1)
}
2021-04-22 19:25:50 +02:00
}
2021-04-22 19:47:06 +02:00
RepeatMode.ALL -> {
2021-08-28 00:02:50 +02:00
play((index + 1) % downloader.playlist.size)
2021-04-22 19:47:06 +02:00
}
2021-04-22 19:25:50 +02:00
RepeatMode.SINGLE -> play(index)
else -> {
}
2020-06-21 09:31:38 +02:00
}
}
2021-04-22 19:25:50 +02:00
null
}
2020-06-21 09:31:38 +02:00
}
2021-04-22 19:25:50 +02:00
@Synchronized
fun clear(serialize: Boolean) {
2021-04-22 19:47:06 +02:00
localMediaPlayer.reset()
downloader.clearPlaylist()
2021-04-22 19:47:06 +02:00
localMediaPlayer.setCurrentPlaying(null)
2021-04-22 19:25:50 +02:00
setNextPlaying()
2020-06-23 18:40:44 +02:00
if (serialize) {
2021-04-22 19:47:06 +02:00
downloadQueueSerializer.serializeDownloadQueue(
2021-08-28 00:02:50 +02:00
downloader.playlist,
2021-04-22 19:47:06 +02:00
downloader.currentPlayingIndex, playerPosition
)
2020-06-21 09:31:38 +02:00
}
}
2021-04-22 19:25:50 +02:00
private fun createNotificationChannel() {
2021-04-21 17:40:51 +02:00
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
2021-04-22 19:47:06 +02:00
// The suggested importance of a startForeground service notification is IMPORTANCE_LOW
val channel = NotificationChannel(
NOTIFICATION_CHANNEL_ID,
NOTIFICATION_CHANNEL_NAME,
NotificationManager.IMPORTANCE_LOW
)
2021-04-22 19:25:50 +02:00
channel.lightColor = android.R.color.holo_blue_dark
channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
channel.setShowBadge(false)
2021-04-22 19:47:06 +02:00
2021-04-22 19:25:50 +02:00
val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
manager.createNotificationChannel(channel)
2021-04-21 17:40:51 +02:00
}
}
2021-04-22 19:25:50 +02:00
fun updateNotification(playerState: PlayerState, currentPlaying: DownloadFile?) {
2021-04-22 19:47:06 +02:00
val notification = buildForegroundNotification(playerState, currentPlaying)
if (Settings.isNotificationEnabled) {
2020-06-25 14:33:44 +02:00
if (isInForeground) {
2020-06-21 09:31:38 +02:00
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
2021-04-22 19:47:06 +02:00
val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
manager.notify(NOTIFICATION_ID, notification)
} else {
2021-04-22 19:47:06 +02:00
val manager = NotificationManagerCompat.from(this)
manager.notify(NOTIFICATION_ID, notification)
2020-06-21 09:31:38 +02:00
}
2021-04-22 19:47:06 +02:00
Timber.v("Updated notification")
} else {
2021-04-22 19:47:06 +02:00
startForeground(NOTIFICATION_ID, notification)
2021-04-22 19:25:50 +02:00
isInForeground = true
2021-04-25 12:24:35 +02:00
Timber.v("Created Foreground notification")
2020-06-21 09:31:38 +02:00
}
}
}
/**
* This method builds a notification, reusing the Notification Builder if possible
*/
@Suppress("SpreadOperator")
2021-04-22 19:47:06 +02:00
private fun buildForegroundNotification(
playerState: PlayerState,
currentPlaying: DownloadFile?
): Notification {
// Init
2021-04-22 19:25:50 +02:00
val context = applicationContext
val song = currentPlaying?.song
2021-07-18 13:17:29 +02:00
val stopIntent = Util.getPendingIntentForMediaAction(
context,
KeyEvent.KEYCODE_MEDIA_STOP,
100
)
2020-06-21 09:31:38 +02:00
// We should use a single notification builder, otherwise the notification may not be updated
if (notificationBuilder == null) {
2021-04-22 19:25:50 +02:00
notificationBuilder = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
2020-06-21 09:31:38 +02:00
// Set some values that never change
2021-04-22 19:25:50 +02:00
notificationBuilder!!.setSmallIcon(R.drawable.ic_stat_ultrasonic)
notificationBuilder!!.setAutoCancel(false)
notificationBuilder!!.setOngoing(true)
notificationBuilder!!.setOnlyAlertOnce(true)
notificationBuilder!!.setWhen(System.currentTimeMillis())
notificationBuilder!!.setShowWhen(false)
notificationBuilder!!.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
notificationBuilder!!.priority = NotificationCompat.PRIORITY_LOW
2020-06-21 09:31:38 +02:00
// Add content intent (when user taps on notification)
2021-04-22 19:47:06 +02:00
notificationBuilder!!.setContentIntent(getPendingIntentForContent())
2021-04-21 17:14:05 +02:00
// This intent is executed when the user closes the notification
2021-04-22 19:25:50 +02:00
notificationBuilder!!.setDeleteIntent(stopIntent)
2020-06-21 09:31:38 +02:00
}
// Use the Media Style, to enable native Android support for playback notification
2021-04-22 19:25:50 +02:00
val style = androidx.media.app.NotificationCompat.MediaStyle()
2021-07-04 22:42:18 +02:00
if (mediaSessionToken != null) {
style.setMediaSession(mediaSessionToken)
}
2020-06-21 09:31:38 +02:00
// Clear old actions
2021-04-22 19:25:50 +02:00
notificationBuilder!!.clearActions()
if (song != null) {
// Add actions
val compactActions = addActions(context, notificationBuilder!!, playerState, song)
// Configure shortcut actions
style.setShowActionsInCompactView(*compactActions)
notificationBuilder!!.setStyle(style)
// Set song title, artist and cover
2021-04-22 19:25:50 +02:00
val iconSize = (256 * context.resources.displayMetrics.density).toInt()
2021-06-07 13:17:00 +02:00
val bitmap = BitmapUtils.getAlbumArtBitmapFromDisk(song, iconSize)
2021-04-22 19:25:50 +02:00
notificationBuilder!!.setContentTitle(song.title)
notificationBuilder!!.setContentText(song.artist)
notificationBuilder!!.setLargeIcon(bitmap)
notificationBuilder!!.setSubText(song.album)
2021-09-23 16:00:20 +02:00
} else if (downloader.started) {
// No song is playing, but Ultrasonic is downloading files
notificationBuilder!!.setContentTitle(
getString(R.string.notification_downloading_title)
)
}
2021-04-22 19:25:50 +02:00
return notificationBuilder!!.build()
}
2021-04-22 19:47:06 +02:00
private fun addActions(
context: Context,
notificationBuilder: NotificationCompat.Builder,
playerState: PlayerState,
song: MusicDirectory.Entry?
): IntArray {
// Init
2021-04-22 19:25:50 +02:00
val compactActionList = ArrayList<Int>()
var numActions = 0 // we start and 0 and then increment by 1 for each call to generateAction
// Star
if (song != null) {
2021-04-22 19:47:06 +02:00
notificationBuilder.addAction(generateStarAction(context, numActions, song.starred))
}
2021-04-22 19:25:50 +02:00
numActions++
// Next
2021-04-22 19:25:50 +02:00
notificationBuilder.addAction(generateAction(context, numActions))
compactActionList.add(numActions)
numActions++
// Play/Pause button
2021-04-22 19:25:50 +02:00
notificationBuilder.addAction(generatePlayPauseAction(context, numActions, playerState))
compactActionList.add(numActions)
numActions++
// Previous
2021-04-22 19:25:50 +02:00
notificationBuilder.addAction(generateAction(context, numActions))
compactActionList.add(numActions)
numActions++
// Close
2021-04-22 19:25:50 +02:00
notificationBuilder.addAction(generateAction(context, numActions))
val actionArray = IntArray(compactActionList.size)
for (i in actionArray.indices) {
actionArray[i] = compactActionList[i]
2020-06-21 09:31:38 +02:00
}
2021-04-22 19:25:50 +02:00
return actionArray
2021-04-22 19:47:06 +02:00
// notificationBuilder.setShowActionsInCompactView())
}
2021-04-22 19:25:50 +02:00
private fun generateAction(context: Context, requestCode: Int): NotificationCompat.Action? {
val keycode: Int
val icon: Int
val label: String
2021-04-22 19:47:06 +02:00
2021-04-22 19:25:50 +02:00
when (requestCode) {
1 -> {
keycode = KeyEvent.KEYCODE_MEDIA_PREVIOUS
label = getString(R.string.common_play_previous)
icon = R.drawable.media_backward_medium_dark
}
2021-04-22 19:47:06 +02:00
2 -> // Is handled in generatePlayPauseAction()
2021-04-22 19:25:50 +02:00
return null
3 -> {
keycode = KeyEvent.KEYCODE_MEDIA_NEXT
label = getString(R.string.common_play_next)
icon = R.drawable.media_forward_medium_dark
}
4 -> {
keycode = KeyEvent.KEYCODE_MEDIA_STOP
label = getString(R.string.buttons_stop)
icon = R.drawable.ic_baseline_close_24
}
else -> return null
}
2021-04-22 19:47:06 +02:00
2021-07-18 11:33:39 +02:00
val pendingIntent = Util.getPendingIntentForMediaAction(context, keycode, requestCode)
2021-04-22 19:25:50 +02:00
return NotificationCompat.Action.Builder(icon, label, pendingIntent).build()
}
2021-04-22 19:47:06 +02:00
private fun generatePlayPauseAction(
context: Context,
requestCode: Int,
playerState: PlayerState
): NotificationCompat.Action {
2021-04-22 19:25:50 +02:00
val isPlaying = playerState === PlayerState.STARTED
2021-04-22 19:47:06 +02:00
val keycode = KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
2021-07-18 11:33:39 +02:00
val pendingIntent = Util.getPendingIntentForMediaAction(context, keycode, requestCode)
2021-04-22 19:25:50 +02:00
val label: String
val icon: Int
2021-04-22 19:47:06 +02:00
if (isPlaying) {
2021-04-22 19:25:50 +02:00
label = getString(R.string.common_pause)
icon = R.drawable.media_pause_large_dark
} else {
2021-04-22 19:25:50 +02:00
label = getString(R.string.common_play)
icon = R.drawable.media_start_large_dark
2020-06-21 09:31:38 +02:00
}
2021-04-22 19:47:06 +02:00
2021-04-22 19:25:50 +02:00
return NotificationCompat.Action.Builder(icon, label, pendingIntent).build()
}
2021-04-22 19:47:06 +02:00
private fun generateStarAction(
context: Context,
requestCode: Int,
isStarred: Boolean
): NotificationCompat.Action {
2021-04-22 19:25:50 +02:00
val label: String
val icon: Int
2021-04-22 19:47:06 +02:00
val keyCode: Int = KeyEvent.KEYCODE_STAR
if (isStarred) {
2021-04-22 19:25:50 +02:00
label = getString(R.string.download_menu_star)
icon = R.drawable.ic_star_full_dark
} else {
2021-04-22 19:25:50 +02:00
label = getString(R.string.download_menu_star)
icon = R.drawable.ic_star_hollow_dark
}
2021-04-22 19:47:06 +02:00
2021-07-18 11:33:39 +02:00
val pendingIntent = Util.getPendingIntentForMediaAction(context, keyCode, requestCode)
2021-04-22 19:25:50 +02:00
return NotificationCompat.Action.Builder(icon, label, pendingIntent).build()
}
2021-04-22 19:47:06 +02:00
private fun getPendingIntentForContent(): PendingIntent {
val intent = Intent(this, NavigationActivity::class.java)
.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
val flags = PendingIntent.FLAG_UPDATE_CURRENT
intent.putExtra(Constants.INTENT_EXTRA_NAME_SHOW_PLAYER, true)
return PendingIntent.getActivity(this, 0, intent, flags)
}
@Suppress("MagicNumber")
2021-04-22 19:25:50 +02:00
companion object {
private const val NOTIFICATION_CHANNEL_ID = "org.moire.ultrasonic"
private const val NOTIFICATION_CHANNEL_NAME = "Ultrasonic background service"
private const val NOTIFICATION_ID = 3033
@Volatile
2021-04-22 19:25:50 +02:00
private var instance: MediaPlayerService? = null
private val instanceLock = Any()
2021-04-22 19:47:06 +02:00
2021-04-22 19:25:50 +02:00
@JvmStatic
fun getInstance(): MediaPlayerService? {
val context = UApp.applicationContext()
2021-10-23 16:13:05 +02:00
// Try for twenty times to retrieve a running service,
// sleep 100 millis between each try,
// and run the block that creates a service only synchronized.
for (i in 0..19) {
if (instance != null) return instance
synchronized(instanceLock) {
2021-04-22 19:25:50 +02:00
if (instance != null) return instance
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
2021-04-22 19:47:06 +02:00
context.startForegroundService(
Intent(context, MediaPlayerService::class.java)
)
2021-04-22 19:25:50 +02:00
} else {
context.startService(Intent(context, MediaPlayerService::class.java))
}
}
Util.sleepQuietly(100L)
2021-04-22 19:25:50 +02:00
}
return instance
2021-04-22 19:25:50 +02:00
}
2021-04-22 19:25:50 +02:00
@JvmStatic
val runningInstance: MediaPlayerService?
get() {
synchronized(instanceLock) { return instance }
}
2021-04-22 19:25:50 +02:00
@JvmStatic
2021-04-22 19:47:06 +02:00
fun executeOnStartedMediaPlayerService(
taskToExecute: (MediaPlayerService) -> Unit
2021-04-22 19:47:06 +02:00
) {
2021-04-22 19:25:50 +02:00
val t: Thread = object : Thread() {
override fun run() {
val instance = getInstance()
2021-04-22 19:25:50 +02:00
if (instance == null) {
2021-04-22 19:47:06 +02:00
Timber.e("ExecuteOnStarted.. failed to get a MediaPlayerService instance!")
2021-04-22 19:25:50 +02:00
return
} else {
taskToExecute(instance)
2021-04-22 19:25:50 +02:00
}
}
}
t.start()
}
}
2021-04-22 19:47:06 +02:00
}