diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerNotifications.kt b/app/src/main/java/org/oxycblt/auxio/music/system/IndexerNotifications.kt
index 3a9a28184..3ef97174e 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerNotifications.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/system/IndexerNotifications.kt
@@ -17,34 +17,20 @@
package org.oxycblt.auxio.music.system
-import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
-import android.os.Build
import androidx.core.app.NotificationCompat
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.IntegerTable
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.newMainPendingIntent
/** The notification responsible for showing the indexer state. */
class IndexingNotification(private val context: Context) :
- NotificationCompat.Builder(context, CHANNEL_ID) {
- private val notificationManager = context.getSystemServiceSafe(NotificationManager::class)
-
+ ServiceNotification(context, INDEXER_CHANNEL) {
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)
setCategory(NotificationCompat.CATEGORY_PROGRESS)
setShowWhen(false)
@@ -56,9 +42,8 @@ class IndexingNotification(private val context: Context) :
setProgress(0, 0, true)
}
- fun renotify() {
- notificationManager.notify(IntegerTable.INDEXER_NOTIFICATION_CODE, build())
- }
+ override val code: Int
+ get() = IntegerTable.INDEXER_NOTIFICATION_CODE
fun updateIndexingState(indexing: Indexer.Indexing): Boolean {
when (indexing) {
@@ -82,27 +67,11 @@ class IndexingNotification(private val context: Context) :
return false
}
-
- companion object {
- const val CHANNEL_ID = BuildConfig.APPLICATION_ID + ".channel.INDEXER"
- }
}
/** The notification responsible for showing the indexer state. */
-class ObservingNotification(context: Context) : NotificationCompat.Builder(context, CHANNEL_ID) {
- private val notificationManager = context.getSystemServiceSafe(NotificationManager::class)
-
+class ObservingNotification(context: Context) : ServiceNotification(context, INDEXER_CHANNEL) {
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)
setCategory(NotificationCompat.CATEGORY_SERVICE)
setShowWhen(false)
@@ -113,11 +82,12 @@ class ObservingNotification(context: Context) : NotificationCompat.Builder(conte
setContentText(context.getString(R.string.lbl_observing_desc))
}
- fun renotify() {
- notificationManager.notify(IntegerTable.INDEXER_NOTIFICATION_CODE, build())
- }
-
- companion object {
- const val CHANNEL_ID = BuildConfig.APPLICATION_ID + ".channel.INDEXER"
- }
+ override val code: Int
+ get() = IntegerTable.INDEXER_NOTIFICATION_CODE
}
+
+private val INDEXER_CHANNEL =
+ ServiceNotification.ChannelInfo(
+ id = BuildConfig.APPLICATION_ID + ".channel.INDEXER",
+ R.string.info_indexer_channel_name,
+ NotificationManager.IMPORTANCE_LOW)
diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt b/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt
index 3e0229c3d..72773e78f 100644
--- a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt
+++ b/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt
@@ -17,7 +17,6 @@
package org.oxycblt.auxio.music.system
-import android.app.Notification
import android.app.Service
import android.content.Intent
import android.database.ContentObserver
@@ -25,17 +24,16 @@ import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.provider.MediaStore
-import androidx.core.app.ServiceCompat
import coil.imageLoader
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
-import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.settings.Settings
+import org.oxycblt.auxio.ui.system.ForegroundManager
import org.oxycblt.auxio.util.contentResolverSafe
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.
*
* @author OxygenCobalt
- *
- * TODO: Add abstractions for services. notifications, and generations
*/
class IndexerService : Service(), Indexer.Controller, Settings.Callback {
private val indexer = Indexer.getInstance()
@@ -63,19 +59,20 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
private val playbackManager = PlaybackStateManager.getInstance()
private lateinit var settings: Settings
- private var isForeground = false
+ private lateinit var foregroundManager: ForegroundManager
private lateinit var indexingNotification: IndexingNotification
private lateinit var observingNotification: ObservingNotification
override fun onCreate() {
super.onCreate()
- settings = Settings(this, this)
- indexerContentObserver = SystemContentObserver()
-
+ foregroundManager = ForegroundManager(this)
indexingNotification = IndexingNotification(this)
observingNotification = ObservingNotification(this)
+ settings = Settings(this, this)
+ indexerContentObserver = SystemContentObserver()
+
indexer.registerController(this)
if (musicStore.library == null && indexer.isIndeterminate) {
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() {
super.onDestroy()
+ foregroundManager.release()
+
// De-initialize the components first to prevent stray reloading events
settings.release()
indexerContentObserver.release()
@@ -162,9 +161,9 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
// notification when initially starting, we will not update the notification
// unless it indicates that we have changed it.
val changed = indexingNotification.updateIndexingState(state)
- if (!tryStartForeground(indexingNotification.build()) && changed) {
+ if (!foregroundManager.tryStartForeground(indexingNotification) && changed) {
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.
// 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.
- if (!tryStartForeground(observingNotification.build())) {
- observingNotification.renotify()
+ if (!foregroundManager.tryStartForeground(observingNotification)) {
+ observingNotification.post()
}
} 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 ---
override fun onSettingChanged(key: String) {
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 cafeaf965..910d21f47 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
@@ -423,8 +423,10 @@ class PlaybackStateManager private constructor() {
isPlaying = false
notifyNewPlayback()
- // Controller may have reloaded the media item, re-seek to the previous position
- seekTo(oldPosition)
+ if (index > -1) {
+ // Controller may have reloaded the media item, re-seek to the previous position
+ seekTo(oldPosition)
+ }
}
private fun makeStateImpl() =
diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt
index 264fc32de..b5551b2f7 100644
--- a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt
+++ b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt
@@ -43,6 +43,9 @@ import org.oxycblt.auxio.util.logD
* using something like MediaSessionConnector is more or less impossible.
*
* @author OxygenCobalt
+ *
+ * TODO: Update textual metadata first, then cover metadata later. Janky, yes, but also resolves
+ * some coherency issues.
*/
class MediaSessionComponent(
private val context: Context,
@@ -54,12 +57,13 @@ class MediaSessionComponent(
PlaybackStateManager.Callback,
Settings.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 settings = Settings(context, this)
- val mediaSession = MediaSessionCompat(context, context.packageName).apply { isActive = true }
private val notification = NotificationComponent(context, mediaSession.sessionToken)
private val provider = BitmapProvider(context)
@@ -98,6 +102,7 @@ class MediaSessionComponent(
private fun updateMediaMetadata(song: Song?, parent: MusicParent?) {
if (song == null) {
mediaSession.setMetadata(emptyMetadata)
+ callback.onPostNotification(null)
return
}
diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/NotificationComponent.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/NotificationComponent.kt
index dce122048..a8a8a2131 100644
--- a/app/src/main/java/org/oxycblt/auxio/playback/system/NotificationComponent.kt
+++ b/app/src/main/java/org/oxycblt/auxio/playback/system/NotificationComponent.kt
@@ -18,7 +18,6 @@
package org.oxycblt.auxio.playback.system
import android.annotation.SuppressLint
-import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.os.Build
@@ -31,7 +30,7 @@ import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.R
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.newMainPendingIntent
@@ -43,20 +42,8 @@ import org.oxycblt.auxio.util.newMainPendingIntent
*/
@SuppressLint("RestrictedApi")
class NotificationComponent(private val context: Context, sessionToken: MediaSessionCompat.Token) :
- NotificationCompat.Builder(context, CHANNEL_ID) {
- private val notificationManager = context.getSystemServiceSafe(NotificationManager::class)
-
+ ServiceNotification(context, CHANNEL_INFO) {
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)
setCategory(NotificationCompat.CATEGORY_TRANSPORT)
setShowWhen(false)
@@ -75,9 +62,8 @@ class NotificationComponent(private val context: Context, sessionToken: MediaSes
setStyle(MediaStyle().setMediaSession(sessionToken).setShowActionsInCompactView(1, 2, 3))
}
- fun renotify() {
- notificationManager.notify(IntegerTable.PLAYBACK_NOTIFICATION_CODE, build())
- }
+ override val code: Int
+ get() = IntegerTable.PLAYBACK_NOTIFICATION_CODE
// --- STATE FUNCTIONS ---
@@ -164,6 +150,10 @@ class NotificationComponent(private val context: Context, sessionToken: MediaSes
}
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)
}
}
diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt
index 0bf123a49..add539b49 100644
--- a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt
+++ b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt
@@ -24,7 +24,6 @@ import android.content.Intent
import android.content.IntentFilter
import android.media.AudioManager
import android.os.IBinder
-import androidx.core.app.ServiceCompat
import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem
@@ -45,7 +44,6 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.oxycblt.auxio.BuildConfig
-import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.Song
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.RepeatMode
import org.oxycblt.auxio.settings.Settings
+import org.oxycblt.auxio.ui.system.ForegroundManager
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.widgets.WidgetComponent
import org.oxycblt.auxio.widgets.WidgetProvider
@@ -93,7 +92,7 @@ class PlaybackService :
private lateinit var settings: Settings
// State
- private var isForeground = false
+ private lateinit var foregroundManager: ForegroundManager
private var hasPlayed = false
// Coroutines
@@ -106,6 +105,8 @@ class PlaybackService :
override fun onCreate() {
super.onCreate()
+ foregroundManager = ForegroundManager(this)
+
// --- PLAYER SETUP ---
replayGainProcessor = ReplayGainAudioProcessor(this)
@@ -163,8 +164,7 @@ class PlaybackService :
override fun onDestroy() {
super.onDestroy()
- ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
- isForeground = false
+ foregroundManager.release()
// Pause just in case this destruction was unexpected.
playbackManager.isPlaying = false
@@ -272,14 +272,17 @@ class PlaybackService :
player.playWhenReady = isPlaying
}
- override fun onPostNotification(notification: NotificationComponent) {
- if (hasPlayed && playbackManager.song != null) {
- if (!isForeground) {
- startForeground(IntegerTable.PLAYBACK_NOTIFICATION_CODE, notification.build())
- isForeground = true
- } else {
- // If we are already in foreground just update the notification
- notification.renotify()
+ override fun onPostNotification(notification: NotificationComponent?) {
+ if (notification == null) {
+ // This case is only here if I ever need to move foreground stopping from
+ // the player code to the notification code.
+ logD("No notification, ignoring")
+ return
+ }
+
+ if (hasPlayed) {
+ if (!foregroundManager.tryStartForeground(notification)) {
+ notification.post()
}
}
}
@@ -329,10 +332,11 @@ class PlaybackService :
/** Stop the foreground state and hide the notification */
private fun stopAndSave() {
- ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
- isForeground = false
- saveScope.launch {
- playbackManager.saveState(PlaybackStateDatabase.getInstance(this@PlaybackService))
+ if (foregroundManager.tryStopForeground()) {
+ logD("Saving playback state")
+ saveScope.launch {
+ playbackManager.saveState(PlaybackStateDatabase.getInstance(this@PlaybackService))
+ }
}
}
diff --git a/app/src/main/java/org/oxycblt/auxio/ui/system/ForegroundManager.kt b/app/src/main/java/org/oxycblt/auxio/ui/system/ForegroundManager.kt
new file mode 100644
index 000000000..685525fc4
--- /dev/null
+++ b/app/src/main/java/org/oxycblt/auxio/ui/system/ForegroundManager.kt
@@ -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 .
+ */
+
+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
+ }
+}
diff --git a/app/src/main/java/org/oxycblt/auxio/ui/system/ServiceNotification.kt b/app/src/main/java/org/oxycblt/auxio/ui/system/ServiceNotification.kt
new file mode 100644
index 000000000..6c51e86ae
--- /dev/null
+++ b/app/src/main/java/org/oxycblt/auxio/ui/system/ServiceNotification.kt
@@ -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 .
+ */
+
+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)
+}