ui: add service/notification abstractions
Add additional abstractions for service foreground states and their notifications.
This commit is contained in:
parent
a63b3791d2
commit
caa755c12f
8 changed files with 188 additions and 117 deletions
|
@ -17,34 +17,20 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.music.system
|
package org.oxycblt.auxio.music.system
|
||||||
|
|
||||||
import android.app.NotificationChannel
|
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import org.oxycblt.auxio.BuildConfig
|
import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.IntegerTable
|
import org.oxycblt.auxio.IntegerTable
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.util.getSystemServiceSafe
|
import org.oxycblt.auxio.ui.system.ServiceNotification
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.newMainPendingIntent
|
import org.oxycblt.auxio.util.newMainPendingIntent
|
||||||
|
|
||||||
/** The notification responsible for showing the indexer state. */
|
/** The notification responsible for showing the indexer state. */
|
||||||
class IndexingNotification(private val context: Context) :
|
class IndexingNotification(private val context: Context) :
|
||||||
NotificationCompat.Builder(context, CHANNEL_ID) {
|
ServiceNotification(context, INDEXER_CHANNEL) {
|
||||||
private val notificationManager = context.getSystemServiceSafe(NotificationManager::class)
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
val channel =
|
|
||||||
NotificationChannel(
|
|
||||||
CHANNEL_ID,
|
|
||||||
context.getString(R.string.info_indexer_channel_name),
|
|
||||||
NotificationManager.IMPORTANCE_LOW)
|
|
||||||
|
|
||||||
notificationManager.createNotificationChannel(channel)
|
|
||||||
}
|
|
||||||
|
|
||||||
setSmallIcon(R.drawable.ic_indexer_24)
|
setSmallIcon(R.drawable.ic_indexer_24)
|
||||||
setCategory(NotificationCompat.CATEGORY_PROGRESS)
|
setCategory(NotificationCompat.CATEGORY_PROGRESS)
|
||||||
setShowWhen(false)
|
setShowWhen(false)
|
||||||
|
@ -56,9 +42,8 @@ class IndexingNotification(private val context: Context) :
|
||||||
setProgress(0, 0, true)
|
setProgress(0, 0, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun renotify() {
|
override val code: Int
|
||||||
notificationManager.notify(IntegerTable.INDEXER_NOTIFICATION_CODE, build())
|
get() = IntegerTable.INDEXER_NOTIFICATION_CODE
|
||||||
}
|
|
||||||
|
|
||||||
fun updateIndexingState(indexing: Indexer.Indexing): Boolean {
|
fun updateIndexingState(indexing: Indexer.Indexing): Boolean {
|
||||||
when (indexing) {
|
when (indexing) {
|
||||||
|
@ -82,27 +67,11 @@ class IndexingNotification(private val context: Context) :
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val CHANNEL_ID = BuildConfig.APPLICATION_ID + ".channel.INDEXER"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The notification responsible for showing the indexer state. */
|
/** The notification responsible for showing the indexer state. */
|
||||||
class ObservingNotification(context: Context) : NotificationCompat.Builder(context, CHANNEL_ID) {
|
class ObservingNotification(context: Context) : ServiceNotification(context, INDEXER_CHANNEL) {
|
||||||
private val notificationManager = context.getSystemServiceSafe(NotificationManager::class)
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
val channel =
|
|
||||||
NotificationChannel(
|
|
||||||
CHANNEL_ID,
|
|
||||||
context.getString(R.string.info_indexer_channel_name),
|
|
||||||
NotificationManager.IMPORTANCE_LOW)
|
|
||||||
|
|
||||||
notificationManager.createNotificationChannel(channel)
|
|
||||||
}
|
|
||||||
|
|
||||||
setSmallIcon(R.drawable.ic_indexer_24)
|
setSmallIcon(R.drawable.ic_indexer_24)
|
||||||
setCategory(NotificationCompat.CATEGORY_SERVICE)
|
setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||||
setShowWhen(false)
|
setShowWhen(false)
|
||||||
|
@ -113,11 +82,12 @@ class ObservingNotification(context: Context) : NotificationCompat.Builder(conte
|
||||||
setContentText(context.getString(R.string.lbl_observing_desc))
|
setContentText(context.getString(R.string.lbl_observing_desc))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun renotify() {
|
override val code: Int
|
||||||
notificationManager.notify(IntegerTable.INDEXER_NOTIFICATION_CODE, build())
|
get() = IntegerTable.INDEXER_NOTIFICATION_CODE
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
private val INDEXER_CHANNEL =
|
||||||
const val CHANNEL_ID = BuildConfig.APPLICATION_ID + ".channel.INDEXER"
|
ServiceNotification.ChannelInfo(
|
||||||
}
|
id = BuildConfig.APPLICATION_ID + ".channel.INDEXER",
|
||||||
}
|
R.string.info_indexer_channel_name,
|
||||||
|
NotificationManager.IMPORTANCE_LOW)
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.music.system
|
package org.oxycblt.auxio.music.system
|
||||||
|
|
||||||
import android.app.Notification
|
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.database.ContentObserver
|
import android.database.ContentObserver
|
||||||
|
@ -25,17 +24,16 @@ import android.os.Handler
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import androidx.core.app.ServiceCompat
|
|
||||||
import coil.imageLoader
|
import coil.imageLoader
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.oxycblt.auxio.IntegerTable
|
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.music.MusicStore
|
import org.oxycblt.auxio.music.MusicStore
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||||
import org.oxycblt.auxio.settings.Settings
|
import org.oxycblt.auxio.settings.Settings
|
||||||
|
import org.oxycblt.auxio.ui.system.ForegroundManager
|
||||||
import org.oxycblt.auxio.util.contentResolverSafe
|
import org.oxycblt.auxio.util.contentResolverSafe
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
|
@ -49,8 +47,6 @@ import org.oxycblt.auxio.util.logD
|
||||||
* boilerplate you skip is not worth the insanity of androidx.
|
* boilerplate you skip is not worth the insanity of androidx.
|
||||||
*
|
*
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*
|
|
||||||
* TODO: Add abstractions for services. notifications, and generations
|
|
||||||
*/
|
*/
|
||||||
class IndexerService : Service(), Indexer.Controller, Settings.Callback {
|
class IndexerService : Service(), Indexer.Controller, Settings.Callback {
|
||||||
private val indexer = Indexer.getInstance()
|
private val indexer = Indexer.getInstance()
|
||||||
|
@ -63,19 +59,20 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
|
||||||
private val playbackManager = PlaybackStateManager.getInstance()
|
private val playbackManager = PlaybackStateManager.getInstance()
|
||||||
private lateinit var settings: Settings
|
private lateinit var settings: Settings
|
||||||
|
|
||||||
private var isForeground = false
|
private lateinit var foregroundManager: ForegroundManager
|
||||||
private lateinit var indexingNotification: IndexingNotification
|
private lateinit var indexingNotification: IndexingNotification
|
||||||
private lateinit var observingNotification: ObservingNotification
|
private lateinit var observingNotification: ObservingNotification
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
settings = Settings(this, this)
|
foregroundManager = ForegroundManager(this)
|
||||||
indexerContentObserver = SystemContentObserver()
|
|
||||||
|
|
||||||
indexingNotification = IndexingNotification(this)
|
indexingNotification = IndexingNotification(this)
|
||||||
observingNotification = ObservingNotification(this)
|
observingNotification = ObservingNotification(this)
|
||||||
|
|
||||||
|
settings = Settings(this, this)
|
||||||
|
indexerContentObserver = SystemContentObserver()
|
||||||
|
|
||||||
indexer.registerController(this)
|
indexer.registerController(this)
|
||||||
if (musicStore.library == null && indexer.isIndeterminate) {
|
if (musicStore.library == null && indexer.isIndeterminate) {
|
||||||
logD("No library present and no previous response, indexing music now")
|
logD("No library present and no previous response, indexing music now")
|
||||||
|
@ -92,6 +89,8 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
|
||||||
|
foregroundManager.release()
|
||||||
|
|
||||||
// De-initialize the components first to prevent stray reloading events
|
// De-initialize the components first to prevent stray reloading events
|
||||||
settings.release()
|
settings.release()
|
||||||
indexerContentObserver.release()
|
indexerContentObserver.release()
|
||||||
|
@ -162,9 +161,9 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
|
||||||
// notification when initially starting, we will not update the notification
|
// notification when initially starting, we will not update the notification
|
||||||
// unless it indicates that we have changed it.
|
// unless it indicates that we have changed it.
|
||||||
val changed = indexingNotification.updateIndexingState(state)
|
val changed = indexingNotification.updateIndexingState(state)
|
||||||
if (!tryStartForeground(indexingNotification.build()) && changed) {
|
if (!foregroundManager.tryStartForeground(indexingNotification) && changed) {
|
||||||
logD("Notification changed, re-posting notification")
|
logD("Notification changed, re-posting notification")
|
||||||
indexingNotification.renotify()
|
indexingNotification.post()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,34 +175,14 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
|
||||||
// we can go foreground later.
|
// we can go foreground later.
|
||||||
// 2. If a non-foreground service is killed, the app will probably still be alive,
|
// 2. If a non-foreground service is killed, the app will probably still be alive,
|
||||||
// and thus the music library will not be updated at all.
|
// and thus the music library will not be updated at all.
|
||||||
if (!tryStartForeground(observingNotification.build())) {
|
if (!foregroundManager.tryStartForeground(observingNotification)) {
|
||||||
observingNotification.renotify()
|
observingNotification.post()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
tryStopForeground()
|
foregroundManager.tryStopForeground()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun tryStartForeground(notification: Notification): Boolean {
|
|
||||||
if (isForeground) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
startForeground(IntegerTable.INDEXER_NOTIFICATION_CODE, notification)
|
|
||||||
isForeground = true
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun tryStopForeground() {
|
|
||||||
if (!isForeground) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
|
||||||
isForeground = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- SETTING CALLBACKS ---
|
// --- SETTING CALLBACKS ---
|
||||||
|
|
||||||
override fun onSettingChanged(key: String) {
|
override fun onSettingChanged(key: String) {
|
||||||
|
|
|
@ -423,9 +423,11 @@ class PlaybackStateManager private constructor() {
|
||||||
isPlaying = false
|
isPlaying = false
|
||||||
notifyNewPlayback()
|
notifyNewPlayback()
|
||||||
|
|
||||||
|
if (index > -1) {
|
||||||
// Controller may have reloaded the media item, re-seek to the previous position
|
// Controller may have reloaded the media item, re-seek to the previous position
|
||||||
seekTo(oldPosition)
|
seekTo(oldPosition)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun makeStateImpl() =
|
private fun makeStateImpl() =
|
||||||
PlaybackStateDatabase.SavedState(
|
PlaybackStateDatabase.SavedState(
|
||||||
|
|
|
@ -43,6 +43,9 @@ import org.oxycblt.auxio.util.logD
|
||||||
* using something like MediaSessionConnector is more or less impossible.
|
* using something like MediaSessionConnector is more or less impossible.
|
||||||
*
|
*
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
|
*
|
||||||
|
* TODO: Update textual metadata first, then cover metadata later. Janky, yes, but also resolves
|
||||||
|
* some coherency issues.
|
||||||
*/
|
*/
|
||||||
class MediaSessionComponent(
|
class MediaSessionComponent(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
|
@ -54,12 +57,13 @@ class MediaSessionComponent(
|
||||||
PlaybackStateManager.Callback,
|
PlaybackStateManager.Callback,
|
||||||
Settings.Callback {
|
Settings.Callback {
|
||||||
interface Callback {
|
interface Callback {
|
||||||
fun onPostNotification(notification: NotificationComponent)
|
fun onPostNotification(notification: NotificationComponent?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val mediaSession = MediaSessionCompat(context, context.packageName).apply { isActive = true }
|
||||||
|
|
||||||
private val playbackManager = PlaybackStateManager.getInstance()
|
private val playbackManager = PlaybackStateManager.getInstance()
|
||||||
private val settings = Settings(context, this)
|
private val settings = Settings(context, this)
|
||||||
val mediaSession = MediaSessionCompat(context, context.packageName).apply { isActive = true }
|
|
||||||
private val notification = NotificationComponent(context, mediaSession.sessionToken)
|
private val notification = NotificationComponent(context, mediaSession.sessionToken)
|
||||||
private val provider = BitmapProvider(context)
|
private val provider = BitmapProvider(context)
|
||||||
|
|
||||||
|
@ -98,6 +102,7 @@ class MediaSessionComponent(
|
||||||
private fun updateMediaMetadata(song: Song?, parent: MusicParent?) {
|
private fun updateMediaMetadata(song: Song?, parent: MusicParent?) {
|
||||||
if (song == null) {
|
if (song == null) {
|
||||||
mediaSession.setMetadata(emptyMetadata)
|
mediaSession.setMetadata(emptyMetadata)
|
||||||
|
callback.onPostNotification(null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,6 @@
|
||||||
package org.oxycblt.auxio.playback.system
|
package org.oxycblt.auxio.playback.system
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.NotificationChannel
|
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
@ -31,7 +30,7 @@ import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.IntegerTable
|
import org.oxycblt.auxio.IntegerTable
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||||
import org.oxycblt.auxio.util.getSystemServiceSafe
|
import org.oxycblt.auxio.ui.system.ServiceNotification
|
||||||
import org.oxycblt.auxio.util.newBroadcastPendingIntent
|
import org.oxycblt.auxio.util.newBroadcastPendingIntent
|
||||||
import org.oxycblt.auxio.util.newMainPendingIntent
|
import org.oxycblt.auxio.util.newMainPendingIntent
|
||||||
|
|
||||||
|
@ -43,20 +42,8 @@ import org.oxycblt.auxio.util.newMainPendingIntent
|
||||||
*/
|
*/
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
class NotificationComponent(private val context: Context, sessionToken: MediaSessionCompat.Token) :
|
class NotificationComponent(private val context: Context, sessionToken: MediaSessionCompat.Token) :
|
||||||
NotificationCompat.Builder(context, CHANNEL_ID) {
|
ServiceNotification(context, CHANNEL_INFO) {
|
||||||
private val notificationManager = context.getSystemServiceSafe(NotificationManager::class)
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
val channel =
|
|
||||||
NotificationChannel(
|
|
||||||
CHANNEL_ID,
|
|
||||||
context.getString(R.string.info_playback_channel_name),
|
|
||||||
NotificationManager.IMPORTANCE_DEFAULT)
|
|
||||||
|
|
||||||
notificationManager.createNotificationChannel(channel)
|
|
||||||
}
|
|
||||||
|
|
||||||
setSmallIcon(R.drawable.ic_auxio_24)
|
setSmallIcon(R.drawable.ic_auxio_24)
|
||||||
setCategory(NotificationCompat.CATEGORY_TRANSPORT)
|
setCategory(NotificationCompat.CATEGORY_TRANSPORT)
|
||||||
setShowWhen(false)
|
setShowWhen(false)
|
||||||
|
@ -75,9 +62,8 @@ class NotificationComponent(private val context: Context, sessionToken: MediaSes
|
||||||
setStyle(MediaStyle().setMediaSession(sessionToken).setShowActionsInCompactView(1, 2, 3))
|
setStyle(MediaStyle().setMediaSession(sessionToken).setShowActionsInCompactView(1, 2, 3))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun renotify() {
|
override val code: Int
|
||||||
notificationManager.notify(IntegerTable.PLAYBACK_NOTIFICATION_CODE, build())
|
get() = IntegerTable.PLAYBACK_NOTIFICATION_CODE
|
||||||
}
|
|
||||||
|
|
||||||
// --- STATE FUNCTIONS ---
|
// --- STATE FUNCTIONS ---
|
||||||
|
|
||||||
|
@ -164,6 +150,10 @@ class NotificationComponent(private val context: Context, sessionToken: MediaSes
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val CHANNEL_ID = BuildConfig.APPLICATION_ID + ".channel.PLAYBACK"
|
val CHANNEL_INFO =
|
||||||
|
ChannelInfo(
|
||||||
|
id = BuildConfig.APPLICATION_ID + ".channel.PLAYBACK",
|
||||||
|
nameRes = R.string.info_playback_channel_name,
|
||||||
|
importance = NotificationManager.IMPORTANCE_LOW)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,6 @@ import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import androidx.core.app.ServiceCompat
|
|
||||||
import com.google.android.exoplayer2.C
|
import com.google.android.exoplayer2.C
|
||||||
import com.google.android.exoplayer2.ExoPlayer
|
import com.google.android.exoplayer2.ExoPlayer
|
||||||
import com.google.android.exoplayer2.MediaItem
|
import com.google.android.exoplayer2.MediaItem
|
||||||
|
@ -45,7 +44,6 @@ import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.oxycblt.auxio.BuildConfig
|
import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.IntegerTable
|
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.replaygain.ReplayGainAudioProcessor
|
import org.oxycblt.auxio.playback.replaygain.ReplayGainAudioProcessor
|
||||||
|
@ -53,6 +51,7 @@ import org.oxycblt.auxio.playback.state.PlaybackStateDatabase
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||||
import org.oxycblt.auxio.settings.Settings
|
import org.oxycblt.auxio.settings.Settings
|
||||||
|
import org.oxycblt.auxio.ui.system.ForegroundManager
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.widgets.WidgetComponent
|
import org.oxycblt.auxio.widgets.WidgetComponent
|
||||||
import org.oxycblt.auxio.widgets.WidgetProvider
|
import org.oxycblt.auxio.widgets.WidgetProvider
|
||||||
|
@ -93,7 +92,7 @@ class PlaybackService :
|
||||||
private lateinit var settings: Settings
|
private lateinit var settings: Settings
|
||||||
|
|
||||||
// State
|
// State
|
||||||
private var isForeground = false
|
private lateinit var foregroundManager: ForegroundManager
|
||||||
private var hasPlayed = false
|
private var hasPlayed = false
|
||||||
|
|
||||||
// Coroutines
|
// Coroutines
|
||||||
|
@ -106,6 +105,8 @@ class PlaybackService :
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
|
foregroundManager = ForegroundManager(this)
|
||||||
|
|
||||||
// --- PLAYER SETUP ---
|
// --- PLAYER SETUP ---
|
||||||
|
|
||||||
replayGainProcessor = ReplayGainAudioProcessor(this)
|
replayGainProcessor = ReplayGainAudioProcessor(this)
|
||||||
|
@ -163,8 +164,7 @@ class PlaybackService :
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
|
||||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
foregroundManager.release()
|
||||||
isForeground = false
|
|
||||||
|
|
||||||
// Pause just in case this destruction was unexpected.
|
// Pause just in case this destruction was unexpected.
|
||||||
playbackManager.isPlaying = false
|
playbackManager.isPlaying = false
|
||||||
|
@ -272,14 +272,17 @@ class PlaybackService :
|
||||||
player.playWhenReady = isPlaying
|
player.playWhenReady = isPlaying
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPostNotification(notification: NotificationComponent) {
|
override fun onPostNotification(notification: NotificationComponent?) {
|
||||||
if (hasPlayed && playbackManager.song != null) {
|
if (notification == null) {
|
||||||
if (!isForeground) {
|
// This case is only here if I ever need to move foreground stopping from
|
||||||
startForeground(IntegerTable.PLAYBACK_NOTIFICATION_CODE, notification.build())
|
// the player code to the notification code.
|
||||||
isForeground = true
|
logD("No notification, ignoring")
|
||||||
} else {
|
return
|
||||||
// If we are already in foreground just update the notification
|
}
|
||||||
notification.renotify()
|
|
||||||
|
if (hasPlayed) {
|
||||||
|
if (!foregroundManager.tryStartForeground(notification)) {
|
||||||
|
notification.post()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -329,12 +332,13 @@ class PlaybackService :
|
||||||
|
|
||||||
/** Stop the foreground state and hide the notification */
|
/** Stop the foreground state and hide the notification */
|
||||||
private fun stopAndSave() {
|
private fun stopAndSave() {
|
||||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
if (foregroundManager.tryStopForeground()) {
|
||||||
isForeground = false
|
logD("Saving playback state")
|
||||||
saveScope.launch {
|
saveScope.launch {
|
||||||
playbackManager.saveState(PlaybackStateDatabase.getInstance(this@PlaybackService))
|
playbackManager.saveState(PlaybackStateDatabase.getInstance(this@PlaybackService))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** A [BroadcastReceiver] for receiving general playback events from the system. */
|
/** A [BroadcastReceiver] for receiving general playback events from the system. */
|
||||||
private inner class PlaybackReceiver : BroadcastReceiver() {
|
private inner class PlaybackReceiver : BroadcastReceiver() {
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 Auxio Project
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.oxycblt.auxio.ui.system
|
||||||
|
|
||||||
|
import android.app.Service
|
||||||
|
import androidx.core.app.ServiceCompat
|
||||||
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper to create consistent behavior regarding a service's foreground state.
|
||||||
|
* @author OxygenCobalt
|
||||||
|
*/
|
||||||
|
class ForegroundManager(private val service: Service) {
|
||||||
|
private var isForeground = false
|
||||||
|
|
||||||
|
fun release() {
|
||||||
|
tryStopForeground()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to enter a foreground state. Returns false if already in foreground, returns true
|
||||||
|
* if state was entered.
|
||||||
|
*/
|
||||||
|
fun tryStartForeground(notification: ServiceNotification): Boolean {
|
||||||
|
if (isForeground) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
logD("Starting foreground state")
|
||||||
|
|
||||||
|
service.startForeground(notification.code, notification.build())
|
||||||
|
isForeground = true
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to stop a foreground state. Returns false if already in backend, returns true
|
||||||
|
* if state was stopped.
|
||||||
|
*/
|
||||||
|
fun tryStopForeground(): Boolean {
|
||||||
|
if (!isForeground) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
logD("Stopping foreground state")
|
||||||
|
|
||||||
|
ServiceCompat.stopForeground(service, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||||
|
isForeground = false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 Auxio Project
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.oxycblt.auxio.ui.system
|
||||||
|
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import org.oxycblt.auxio.util.getSystemServiceSafe
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper around [NotificationCompat.Builder] that automates parts of the notification setup.
|
||||||
|
* @author OxygenCobalt
|
||||||
|
*/
|
||||||
|
abstract class ServiceNotification(context: Context, info: ChannelInfo) :
|
||||||
|
NotificationCompat.Builder(context, info.id) {
|
||||||
|
private val notificationManager = context.getSystemServiceSafe(NotificationManager::class)
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
|
||||||
|
val channel =
|
||||||
|
NotificationChannel(info.id, context.getString(info.nameRes), info.importance)
|
||||||
|
|
||||||
|
notificationManager.createNotificationChannel(channel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract val code: Int
|
||||||
|
|
||||||
|
fun post() {
|
||||||
|
notificationManager.notify(code, build())
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ChannelInfo(val id: String, @StringRes val nameRes: Int, val importance: Int)
|
||||||
|
}
|
Loading…
Reference in a new issue