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) +}