From 22a22a883f17cd71203d6506c6591fde5517c789 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sun, 25 Feb 2024 11:26:20 -0700 Subject: [PATCH] service: unify playback and indexer Playback and indexing now occur in the same service through a new bridge called AuxioService. AuxioService contains the existing service instances as Fragment implementations, and then forwards typical service events to them (albeit this will drift more and more as I continue to deal with lifecycle issues). This should be the first step in enabling true service independence, as it means that the service will now immediately initialize and load music as soon as possible. --- app/src/main/AndroidManifest.xml | 14 +--- .../java/org/oxycblt/auxio/MainActivity.kt | 6 +- .../oxycblt/auxio/music/MusicRepository.kt | 4 +- .../IndexerNotifications.kt | 2 +- .../IndexerServiceFragment.kt} | 66 +++++++--------- .../{system => service}/BetterShuffleOrder.kt | 3 +- .../BluetoothHeadsetReceiver.kt | 2 +- .../MediaButtonReceiver.kt | 7 +- .../MediaSessionComponent.kt | 10 +-- .../NotificationComponent.kt | 16 ++-- .../PlaybackServiceFragment.kt} | 78 +++++++------------ .../{system => service}/SystemModule.kt | 2 +- .../org/oxycblt/auxio/service/AuxioService.kt | 68 ++++++++++++++++ .../auxio/service/ForegroundManager.kt | 78 ------------------- .../oxycblt/auxio/service/ServiceFragment.kt | 69 ++++++++++++++++ .../oxycblt/auxio/widgets/WidgetProvider.kt | 12 +-- build.gradle | 2 +- 17 files changed, 229 insertions(+), 210 deletions(-) rename app/src/main/java/org/oxycblt/auxio/music/{system => service}/IndexerNotifications.kt (99%) rename app/src/main/java/org/oxycblt/auxio/music/{system/IndexerService.kt => service/IndexerServiceFragment.kt} (85%) rename app/src/main/java/org/oxycblt/auxio/playback/{system => service}/BetterShuffleOrder.kt (98%) rename app/src/main/java/org/oxycblt/auxio/playback/{system => service}/BluetoothHeadsetReceiver.kt (97%) rename app/src/main/java/org/oxycblt/auxio/playback/{system => service}/MediaButtonReceiver.kt (94%) rename app/src/main/java/org/oxycblt/auxio/playback/{system => service}/MediaSessionComponent.kt (98%) rename app/src/main/java/org/oxycblt/auxio/playback/{system => service}/NotificationComponent.kt (90%) rename app/src/main/java/org/oxycblt/auxio/playback/{system/PlaybackService.kt => service/PlaybackServiceFragment.kt} (93%) rename app/src/main/java/org/oxycblt/auxio/playback/{system => service}/SystemModule.kt (98%) create mode 100644 app/src/main/java/org/oxycblt/auxio/service/AuxioService.kt delete mode 100644 app/src/main/java/org/oxycblt/auxio/service/ForegroundManager.kt create mode 100644 app/src/main/java/org/oxycblt/auxio/service/ServiceFragment.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f7fa7b198..c657ec84b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -77,22 +77,12 @@ - - diff --git a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt index 3fa2ad852..f997f3b0c 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt @@ -29,10 +29,9 @@ import androidx.core.view.updatePadding import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject import org.oxycblt.auxio.databinding.ActivityMainBinding -import org.oxycblt.auxio.music.system.IndexerService import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.state.DeferredPlayback -import org.oxycblt.auxio.playback.system.PlaybackService +import org.oxycblt.auxio.service.AuxioService import org.oxycblt.auxio.ui.UISettings import org.oxycblt.auxio.util.isNight import org.oxycblt.auxio.util.logD @@ -71,8 +70,7 @@ class MainActivity : AppCompatActivity() { override fun onResume() { super.onResume() - startService(Intent(this, IndexerService::class.java)) - startService(Intent(this, PlaybackService::class.java)) + startService(Intent(this, AuxioService::class.java)) if (!startIntentAction(intent)) { // No intent action to do, just restore the previously saved state. diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt index 459c8fcbb..e1b6e2442 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -206,7 +206,7 @@ interface MusicRepository { /** A persistent worker that can load music in the background. */ interface IndexingWorker { /** A [Context] required to read device storage */ - val context: Context + val applicationContext: Context /** The [CoroutineScope] to perform coroutine music loading work on. */ val scope: CoroutineScope @@ -343,7 +343,7 @@ constructor( } override fun index(worker: MusicRepository.IndexingWorker, withCache: Boolean) = - worker.scope.launch { indexWrapper(worker.context, this, withCache) } + worker.scope.launch { indexWrapper(worker.applicationContext, this, withCache) } private suspend fun indexWrapper(context: Context, scope: CoroutineScope, withCache: Boolean) { try { diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerNotifications.kt b/app/src/main/java/org/oxycblt/auxio/music/service/IndexerNotifications.kt similarity index 99% rename from app/src/main/java/org/oxycblt/auxio/music/system/IndexerNotifications.kt rename to app/src/main/java/org/oxycblt/auxio/music/service/IndexerNotifications.kt index e94e4fe16..c70707375 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerNotifications.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/service/IndexerNotifications.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.system +package org.oxycblt.auxio.music.service import android.content.Context import android.os.SystemClock diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt b/app/src/main/java/org/oxycblt/auxio/music/service/IndexerServiceFragment.kt similarity index 85% rename from app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt rename to app/src/main/java/org/oxycblt/auxio/music/service/IndexerServiceFragment.kt index 83f8d5f80..06fd193e4 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/service/IndexerServiceFragment.kt @@ -1,6 +1,6 @@ /* * Copyright (c) 2022 Auxio Project - * IndexerService.kt is part of Auxio. + * IndexerServiceFragment.kt is part of Auxio. * * 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 @@ -16,18 +16,16 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.system +package org.oxycblt.auxio.music.service import android.app.Service -import android.content.Intent +import android.content.Context import android.database.ContentObserver import android.os.Handler -import android.os.IBinder import android.os.Looper import android.os.PowerManager import android.provider.MediaStore import coil.ImageLoader -import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -39,7 +37,7 @@ import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.MusicSettings import org.oxycblt.auxio.music.fs.contentResolverSafe import org.oxycblt.auxio.playback.state.PlaybackStateManager -import org.oxycblt.auxio.service.ForegroundManager +import org.oxycblt.auxio.service.ServiceFragment import org.oxycblt.auxio.util.getSystemServiceCompat import org.oxycblt.auxio.util.logD @@ -57,35 +55,33 @@ import org.oxycblt.auxio.util.logD * * TODO: Unify with PlaybackService as part of the service independence project */ -@AndroidEntryPoint -class IndexerService : - Service(), +class IndexerServiceFragment +@Inject +constructor( + val imageLoader: ImageLoader, + val musicRepository: MusicRepository, + val musicSettings: MusicSettings, + val playbackManager: PlaybackStateManager +) : + ServiceFragment(), MusicRepository.IndexingWorker, MusicRepository.IndexingListener, MusicRepository.UpdateListener, MusicSettings.Listener { - @Inject lateinit var imageLoader: ImageLoader - @Inject lateinit var musicRepository: MusicRepository - @Inject lateinit var musicSettings: MusicSettings - @Inject lateinit var playbackManager: PlaybackStateManager - private val serviceJob = Job() private val indexScope = CoroutineScope(serviceJob + Dispatchers.IO) private var currentIndexJob: Job? = null - private lateinit var foregroundManager: ForegroundManager private lateinit var indexingNotification: IndexingNotification private lateinit var observingNotification: ObservingNotification private lateinit var wakeLock: PowerManager.WakeLock private lateinit var indexerContentObserver: SystemContentObserver - override fun onCreate() { - super.onCreate() - // Initialize the core service components first. - foregroundManager = ForegroundManager(this) - indexingNotification = IndexingNotification(this) - observingNotification = ObservingNotification(this) + override fun onCreate(context: Context) { + indexingNotification = IndexingNotification(context) + observingNotification = ObservingNotification(context) wakeLock = - getSystemServiceCompat(PowerManager::class) + context + .getSystemServiceCompat(PowerManager::class) .newWakeLock( PowerManager.PARTIAL_WAKE_LOCK, BuildConfig.APPLICATION_ID + ":IndexerService") // Initialize any listener-dependent components last as we wouldn't want a listener race @@ -99,14 +95,8 @@ class IndexerService : logD("Service created.") } - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int) = START_NOT_STICKY - - override fun onBind(intent: Intent?): IBinder? = null - override fun onDestroy() { - super.onDestroy() // De-initialize core service components first. - foregroundManager.release() wakeLock.releaseSafe() // Then cancel the listener-dependent components to ensure that stray reloading // events will not occur. @@ -126,10 +116,11 @@ class IndexerService : // Cancel the previous music loading job. currentIndexJob?.cancel() // Start a new music loading job on a co-routine. - currentIndexJob = musicRepository.index(this@IndexerService, withCache) + currentIndexJob = musicRepository.index(this, withCache) } - override val context = this + override val applicationContext: Context + get() = context override val scope = indexScope @@ -169,9 +160,9 @@ class IndexerService : // notification when initially starting, we will not update the notification // unless it indicates that it has changed. val changed = indexingNotification.updateIndexingState(progress) - if (!foregroundManager.tryStartForeground(indexingNotification) && changed) { + if (changed) { logD("Notification changed, re-posting notification") - indexingNotification.post() + startForeground(indexingNotification) } // Make sure we can keep the CPU on while loading music wakeLock.acquireSafe() @@ -188,14 +179,11 @@ class IndexerService : // TODO: Assuming I unify this with PlaybackService, it's possible that I won't need // this anymore, or at least I only have to use it when the app task is not removed. logD("Need to observe, staying in foreground") - if (!foregroundManager.tryStartForeground(observingNotification)) { - logD("Notification changed, re-posting notification") - observingNotification.post() - } + startForeground(observingNotification) } else { // Not observing and done loading, exit foreground. logD("Exiting foreground") - foregroundManager.tryStopForeground() + stopForeground() } // Release our wake lock (if we were using it) wakeLock.releaseSafe() @@ -250,7 +238,7 @@ class IndexerService : private val handler = Handler(Looper.getMainLooper()) init { - contentResolverSafe.registerContentObserver( + context.contentResolverSafe.registerContentObserver( MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, true, this) } @@ -260,7 +248,7 @@ class IndexerService : */ fun release() { handler.removeCallbacks(this) - contentResolverSafe.unregisterContentObserver(this) + context.contentResolverSafe.unregisterContentObserver(this) } override fun onChange(selfChange: Boolean) { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/BetterShuffleOrder.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/BetterShuffleOrder.kt similarity index 98% rename from app/src/main/java/org/oxycblt/auxio/playback/system/BetterShuffleOrder.kt rename to app/src/main/java/org/oxycblt/auxio/playback/service/BetterShuffleOrder.kt index e09d9ba2a..fe5c8628b 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/BetterShuffleOrder.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/BetterShuffleOrder.kt @@ -16,11 +16,10 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.playback.system +package org.oxycblt.auxio.playback.service import androidx.media3.common.C import androidx.media3.exoplayer.source.ShuffleOrder -import java.util.* /** * A ShuffleOrder that fixes the poorly defined default implementation of cloneAndInsert. Whereas diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/BluetoothHeadsetReceiver.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/BluetoothHeadsetReceiver.kt similarity index 97% rename from app/src/main/java/org/oxycblt/auxio/playback/system/BluetoothHeadsetReceiver.kt rename to app/src/main/java/org/oxycblt/auxio/playback/service/BluetoothHeadsetReceiver.kt index c8dbabc83..11df166f2 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/BluetoothHeadsetReceiver.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/BluetoothHeadsetReceiver.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.playback.system +package org.oxycblt.auxio.playback.service import android.bluetooth.BluetoothProfile import android.content.BroadcastReceiver diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaButtonReceiver.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaButtonReceiver.kt similarity index 94% rename from app/src/main/java/org/oxycblt/auxio/playback/system/MediaButtonReceiver.kt rename to app/src/main/java/org/oxycblt/auxio/playback/service/MediaButtonReceiver.kt index 83b6bcbcd..f2281ac49 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaButtonReceiver.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaButtonReceiver.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.playback.system +package org.oxycblt.auxio.playback.service import android.content.BroadcastReceiver import android.content.ComponentName @@ -29,7 +29,8 @@ import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.util.logD /** - * A [BroadcastReceiver] that forwards [Intent.ACTION_MEDIA_BUTTON] [Intent]s to [PlaybackService]. + * A [BroadcastReceiver] that forwards [Intent.ACTION_MEDIA_BUTTON] [Intent]s to + * [PlaybackServiceFragment]. * * @author Alexander Capehart (OxygenCobalt) */ @@ -46,7 +47,7 @@ class MediaButtonReceiver : BroadcastReceiver() { // wrong action at the wrong time will result in the app crashing, and there is // nothing I can do about it. logD("Delivering media button intent $intent") - intent.component = ComponentName(context, PlaybackService::class.java) + intent.component = ComponentName(context, PlaybackServiceFragment::class.java) ContextCompat.startForegroundService(context, intent) } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionComponent.kt similarity index 98% rename from app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt rename to app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionComponent.kt index cbccf56ec..61720d277 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionComponent.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.playback.system +package org.oxycblt.auxio.playback.service import android.content.Context import android.content.Intent @@ -275,7 +275,7 @@ constructor( override fun onStop() { // Get the service to shut down with the ACTION_EXIT intent - context.sendBroadcast(Intent(PlaybackService.ACTION_EXIT)) + context.sendBroadcast(Intent(PlaybackServiceFragment.ACTION_EXIT)) } // --- INTERNAL --- @@ -403,7 +403,7 @@ constructor( ActionMode.SHUFFLE -> { logD("Using shuffle MediaSession action") PlaybackStateCompat.CustomAction.Builder( - PlaybackService.ACTION_INVERT_SHUFFLE, + PlaybackServiceFragment.ACTION_INVERT_SHUFFLE, context.getString(R.string.desc_shuffle), if (playbackManager.isShuffled) { R.drawable.ic_shuffle_on_24 @@ -414,7 +414,7 @@ constructor( else -> { logD("Using repeat mode MediaSession action") PlaybackStateCompat.CustomAction.Builder( - PlaybackService.ACTION_INC_REPEAT_MODE, + PlaybackServiceFragment.ACTION_INC_REPEAT_MODE, context.getString(R.string.desc_change_repeat), playbackManager.repeatMode.icon) } @@ -424,7 +424,7 @@ constructor( // Add the exit action so the service can be closed val exitAction = PlaybackStateCompat.CustomAction.Builder( - PlaybackService.ACTION_EXIT, + PlaybackServiceFragment.ACTION_EXIT, context.getString(R.string.desc_exit), R.drawable.ic_close_24) .build() diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/NotificationComponent.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/NotificationComponent.kt similarity index 90% rename from app/src/main/java/org/oxycblt/auxio/playback/system/NotificationComponent.kt rename to app/src/main/java/org/oxycblt/auxio/playback/service/NotificationComponent.kt index 7b9868072..c441f8bff 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/NotificationComponent.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/NotificationComponent.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.playback.system +package org.oxycblt.auxio.playback.service import android.annotation.SuppressLint import android.content.Context @@ -53,11 +53,13 @@ class NotificationComponent(private val context: Context, sessionToken: MediaSes addAction(buildRepeatAction(context, RepeatMode.NONE)) addAction( - buildAction(context, PlaybackService.ACTION_SKIP_PREV, R.drawable.ic_skip_prev_24)) + buildAction( + context, PlaybackServiceFragment.ACTION_SKIP_PREV, R.drawable.ic_skip_prev_24)) addAction(buildPlayPauseAction(context, true)) addAction( - buildAction(context, PlaybackService.ACTION_SKIP_NEXT, R.drawable.ic_skip_next_24)) - addAction(buildAction(context, PlaybackService.ACTION_EXIT, R.drawable.ic_close_24)) + buildAction( + context, PlaybackServiceFragment.ACTION_SKIP_NEXT, R.drawable.ic_skip_next_24)) + addAction(buildAction(context, PlaybackServiceFragment.ACTION_EXIT, R.drawable.ic_close_24)) setStyle(MediaStyle().setMediaSession(sessionToken).setShowActionsInCompactView(1, 2, 3)) } @@ -122,14 +124,14 @@ class NotificationComponent(private val context: Context, sessionToken: MediaSes } else { R.drawable.ic_play_24 } - return buildAction(context, PlaybackService.ACTION_PLAY_PAUSE, drawableRes) + return buildAction(context, PlaybackServiceFragment.ACTION_PLAY_PAUSE, drawableRes) } private fun buildRepeatAction( context: Context, repeatMode: RepeatMode ): NotificationCompat.Action { - return buildAction(context, PlaybackService.ACTION_INC_REPEAT_MODE, repeatMode.icon) + return buildAction(context, PlaybackServiceFragment.ACTION_INC_REPEAT_MODE, repeatMode.icon) } private fun buildShuffleAction( @@ -142,7 +144,7 @@ class NotificationComponent(private val context: Context, sessionToken: MediaSes } else { R.drawable.ic_shuffle_off_24 } - return buildAction(context, PlaybackService.ACTION_INVERT_SHUFFLE, drawableRes) + return buildAction(context, PlaybackServiceFragment.ACTION_INVERT_SHUFFLE, drawableRes) } private fun buildAction(context: Context, actionName: String, @DrawableRes iconRes: Int) = diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/PlaybackServiceFragment.kt similarity index 93% rename from app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt rename to app/src/main/java/org/oxycblt/auxio/playback/service/PlaybackServiceFragment.kt index ba54a1f18..f30d1f727 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/PlaybackServiceFragment.kt @@ -1,6 +1,6 @@ /* * Copyright (c) 2021 Auxio Project - * PlaybackService.kt is part of Auxio. + * PlaybackServiceFragment.kt is part of Auxio. * * 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 @@ -16,16 +16,14 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.playback.system +package org.oxycblt.auxio.playback.service -import android.app.Service import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.media.AudioManager import android.media.audiofx.AudioEffect -import android.os.IBinder import androidx.core.content.ContextCompat import androidx.media3.common.AudioAttributes import androidx.media3.common.C @@ -39,7 +37,6 @@ import androidx.media3.exoplayer.audio.AudioCapabilities import androidx.media3.exoplayer.audio.MediaCodecAudioRenderer import androidx.media3.exoplayer.mediacodec.MediaCodecSelector import androidx.media3.exoplayer.source.MediaSource -import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -63,7 +60,7 @@ import org.oxycblt.auxio.playback.state.Progression import org.oxycblt.auxio.playback.state.RawQueue import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.state.StateAck -import org.oxycblt.auxio.service.ForegroundManager +import org.oxycblt.auxio.service.ServiceFragment import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.widgets.WidgetComponent @@ -85,9 +82,20 @@ import org.oxycblt.auxio.widgets.WidgetProvider * TODO: Refactor lifecycle to run completely headless (i.e no activity needed) * TODO: Android Auto */ -@AndroidEntryPoint -class PlaybackService : - Service(), +class PlaybackServiceFragment +@Inject +constructor( + val mediaSourceFactory: MediaSource.Factory, + val replayGainProcessor: ReplayGainAudioProcessor, + val mediaSessionComponent: MediaSessionComponent, + val widgetComponent: WidgetComponent, + val playbackManager: PlaybackStateManager, + val playbackSettings: PlaybackSettings, + val persistenceRepository: PersistenceRepository, + val listSettings: ListSettings, + val musicRepository: MusicRepository +) : + ServiceFragment(), Player.Listener, PlaybackStateHolder, PlaybackSettings.Listener, @@ -95,23 +103,11 @@ class PlaybackService : MusicRepository.UpdateListener { // Player components private lateinit var player: ExoPlayer - @Inject lateinit var mediaSourceFactory: MediaSource.Factory - @Inject lateinit var replayGainProcessor: ReplayGainAudioProcessor // System backend components - @Inject lateinit var mediaSessionComponent: MediaSessionComponent - @Inject lateinit var widgetComponent: WidgetComponent private val systemReceiver = PlaybackReceiver() - // Shared components - @Inject lateinit var playbackManager: PlaybackStateManager - @Inject lateinit var playbackSettings: PlaybackSettings - @Inject lateinit var persistenceRepository: PersistenceRepository - @Inject lateinit var listSettings: ListSettings - @Inject lateinit var musicRepository: MusicRepository - - // State - private lateinit var foregroundManager: ForegroundManager + // Stat private var hasPlayed = false private var openAudioEffectSession = false @@ -123,16 +119,14 @@ class PlaybackService : // --- SERVICE OVERRIDES --- - override fun onCreate() { - super.onCreate() - + override fun onCreate(context: Context) { // Since Auxio is a music player, only specify an audio renderer to save // battery/apk size/cache size val audioRenderer = RenderersFactory { handler, _, audioListener, _, _ -> arrayOf( FfmpegAudioRenderer(handler, audioListener, replayGainProcessor), MediaCodecAudioRenderer( - this, + context, MediaCodecSelector.DEFAULT, handler, audioListener, @@ -141,7 +135,7 @@ class PlaybackService : } player = - ExoPlayer.Builder(this, audioRenderer) + ExoPlayer.Builder(context, audioRenderer) .setMediaSourceFactory(mediaSourceFactory) // Enable automatic WakeLock support .setWakeMode(C.WAKE_MODE_LOCAL) @@ -154,7 +148,6 @@ class PlaybackService : true) .build() .also { it.addListener(this) } - foregroundManager = ForegroundManager(this) // Initialize any listener-dependent components last as we wouldn't want a listener race // condition to cause us to load music before we were fully initialize. playbackManager.registerStateHolder(this) @@ -176,23 +169,19 @@ class PlaybackService : } ContextCompat.registerReceiver( - this, systemReceiver, intentFilter, ContextCompat.RECEIVER_EXPORTED) + context, systemReceiver, intentFilter, ContextCompat.RECEIVER_EXPORTED) logD("Service created") } - override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { + override fun onStartCommand(intent: Intent) { // Forward system media button sent by MediaButtonReceiver to MediaSessionComponent if (intent.action == Intent.ACTION_MEDIA_BUTTON) { mediaSessionComponent.handleMediaButtonIntent(intent) } - return START_NOT_STICKY } - override fun onBind(intent: Intent): IBinder? = null - - override fun onTaskRemoved(rootIntent: Intent?) { - super.onTaskRemoved(rootIntent) + override fun onTaskRemoved() { if (!playbackManager.progression.isPlaying) { playbackManager.playing(false) endSession() @@ -200,17 +189,13 @@ class PlaybackService : } override fun onDestroy() { - super.onDestroy() - - foregroundManager.release() - // Pause just in case this destruction was unexpected. playbackManager.playing(false) playbackManager.unregisterStateHolder(this) musicRepository.removeUpdateListener(this) playbackSettings.unregisterListener(this) - unregisterReceiver(systemReceiver) + context.unregisterReceiver(systemReceiver) serviceJob.cancel() widgetComponent.release() @@ -454,7 +439,7 @@ class PlaybackService : // Open -> Try to find the Song for the given file and then play it from all songs is DeferredPlayback.Open -> { logD("Opening specified file") - deviceLibrary.findSongForUri(application, action.uri)?.let { song -> + deviceLibrary.findSongForUri(context.applicationContext, action.uri)?.let { song -> playbackManager.play( song, null, @@ -579,10 +564,7 @@ class PlaybackService : // manner. if (hasPlayed) { logD("Played before, starting foreground state") - if (!foregroundManager.tryStartForeground(notification)) { - logD("Notification changed, re-posting") - notification.post() - } + startForeground(notification) } } @@ -659,9 +641,9 @@ class PlaybackService : private fun broadcastAudioEffectAction(event: String) { logD("Broadcasting AudioEffect event: $event") - sendBroadcast( + context.sendBroadcast( Intent(event) - .putExtra(AudioEffect.EXTRA_PACKAGE_NAME, packageName) + .putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.packageName) .putExtra(AudioEffect.EXTRA_AUDIO_SESSION, audioSessionId) .putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC)) } @@ -678,7 +660,7 @@ class PlaybackService : if (!player.isPlaying) { hasPlayed = false playbackManager.playing(false) - foregroundManager.tryStopForeground() + stopForeground() } } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/SystemModule.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/SystemModule.kt similarity index 98% rename from app/src/main/java/org/oxycblt/auxio/playback/system/SystemModule.kt rename to app/src/main/java/org/oxycblt/auxio/playback/service/SystemModule.kt index 47b052761..3aade8f3e 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/SystemModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/SystemModule.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.playback.system +package org.oxycblt.auxio.playback.service import android.content.Context import androidx.media3.datasource.ContentDataSource diff --git a/app/src/main/java/org/oxycblt/auxio/service/AuxioService.kt b/app/src/main/java/org/oxycblt/auxio/service/AuxioService.kt new file mode 100644 index 000000000..eb782fca4 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/service/AuxioService.kt @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2024 Auxio Project + * AuxioService.kt is part of Auxio. + * + * 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.service + +import android.app.Service +import android.content.Intent +import androidx.core.app.ServiceCompat +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject +import org.oxycblt.auxio.music.service.IndexerServiceFragment +import org.oxycblt.auxio.playback.service.PlaybackServiceFragment + +@AndroidEntryPoint +class AuxioService : Service() { + @Inject lateinit var playbackFragment: PlaybackServiceFragment + @Inject lateinit var indexerFragment: IndexerServiceFragment + + override fun onBind(intent: Intent?) = null + + override fun onCreate() { + super.onCreate() + playbackFragment.attach(this) + indexerFragment.attach(this) + } + + override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { + playbackFragment.handleIntent(intent) + indexerFragment.handleIntent(intent) + return START_NOT_STICKY + } + + override fun onTaskRemoved(rootIntent: Intent?) { + super.onTaskRemoved(rootIntent) + playbackFragment.handleTaskRemoved() + indexerFragment.handleTaskRemoved() + } + + override fun onDestroy() { + super.onDestroy() + playbackFragment.release() + indexerFragment.release() + } + + fun refreshForeground() { + val currentNotification = playbackFragment.notification ?: indexerFragment.notification + if (currentNotification != null) { + startForeground(currentNotification.code, currentNotification.build()) + } else { + ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_DETACH) + } + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/service/ForegroundManager.kt b/app/src/main/java/org/oxycblt/auxio/service/ForegroundManager.kt deleted file mode 100644 index b23457d48..000000000 --- a/app/src/main/java/org/oxycblt/auxio/service/ForegroundManager.kt +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (c) 2022 Auxio Project - * ForegroundManager.kt is part of Auxio. - * - * 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.service - -import android.app.Service -import androidx.core.app.ServiceCompat -import org.oxycblt.auxio.util.logD - -/** - * A utility to create consistent foreground behavior for a given [Service]. - * - * @param service [Service] to wrap in this instance. - * @author Alexander Capehart (OxygenCobalt) - * - * TODO: Merge with unified service when done. - */ -class ForegroundManager(private val service: Service) { - private var isForeground = false - - /** Release this instance. */ - fun release() { - tryStopForeground() - } - - /** - * Try to enter a foreground state. - * - * @param notification The [ForegroundServiceNotification] to show in order to signal the - * foreground state. - * @return true if the state was changed, false otherwise - * @see Service.startForeground - */ - fun tryStartForeground(notification: ForegroundServiceNotification): Boolean { - if (isForeground) { - // Nothing to do. - return false - } - - logD("Starting foreground state") - service.startForeground(notification.code, notification.build()) - isForeground = true - return true - } - - /** - * Try to exit a foreground state. Will remove the foreground notification. - * - * @return true if the state was changed, false otherwise - * @see Service.stopForeground - */ - fun tryStopForeground(): Boolean { - if (!isForeground) { - // Nothing to do. - 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/service/ServiceFragment.kt b/app/src/main/java/org/oxycblt/auxio/service/ServiceFragment.kt new file mode 100644 index 000000000..b52adf629 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/service/ServiceFragment.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2024 Auxio Project + * ServiceFragment.kt is part of Auxio. + * + * 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.service + +import android.content.Context +import android.content.Intent + +abstract class ServiceFragment { + private var handle: AuxioService? = null + + protected val context: Context + get() = requireNotNull(handle) + + var notification: ForegroundServiceNotification? = null + private set + + fun attach(handle: AuxioService) { + this.handle = handle + onCreate(handle) + } + + fun release() { + notification = null + handle = null + onDestroy() + } + + fun handleIntent(intent: Intent) { + onStartCommand(intent) + } + + fun handleTaskRemoved() { + onTaskRemoved() + } + + protected open fun onCreate(context: Context) {} + + protected open fun onDestroy() {} + + protected open fun onStartCommand(intent: Intent) {} + + protected open fun onTaskRemoved() {} + + protected fun startForeground(notification: ForegroundServiceNotification) { + this.notification = notification + requireNotNull(handle).refreshForeground() + } + + protected fun stopForeground() { + this.notification = null + requireNotNull(handle).refreshForeground() + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt index 7a3bc6c40..3f14578f6 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt @@ -32,7 +32,7 @@ import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.R import org.oxycblt.auxio.music.resolveNames import org.oxycblt.auxio.playback.state.RepeatMode -import org.oxycblt.auxio.playback.system.PlaybackService +import org.oxycblt.auxio.playback.service.PlaybackServiceFragment import org.oxycblt.auxio.ui.UISettings import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logW @@ -323,7 +323,7 @@ class WidgetProvider : AppWidgetProvider() { // by PlaybackService. setOnClickPendingIntent( R.id.widget_play_pause, - context.newBroadcastPendingIntent(PlaybackService.ACTION_PLAY_PAUSE)) + context.newBroadcastPendingIntent(PlaybackServiceFragment.ACTION_PLAY_PAUSE)) // Set up the play/pause button appearance. Like the Android 13 media controls, use // a circular FAB when paused, and a squircle FAB when playing. This does require us @@ -364,10 +364,10 @@ class WidgetProvider : AppWidgetProvider() { // by PlaybackService. setOnClickPendingIntent( R.id.widget_skip_prev, - context.newBroadcastPendingIntent(PlaybackService.ACTION_SKIP_PREV)) + context.newBroadcastPendingIntent(PlaybackServiceFragment.ACTION_SKIP_PREV)) setOnClickPendingIntent( R.id.widget_skip_next, - context.newBroadcastPendingIntent(PlaybackService.ACTION_SKIP_NEXT)) + context.newBroadcastPendingIntent(PlaybackServiceFragment.ACTION_SKIP_NEXT)) return this } @@ -389,10 +389,10 @@ class WidgetProvider : AppWidgetProvider() { // be recognized by PlaybackService. setOnClickPendingIntent( R.id.widget_repeat, - context.newBroadcastPendingIntent(PlaybackService.ACTION_INC_REPEAT_MODE)) + context.newBroadcastPendingIntent(PlaybackServiceFragment.ACTION_INC_REPEAT_MODE)) setOnClickPendingIntent( R.id.widget_shuffle, - context.newBroadcastPendingIntent(PlaybackService.ACTION_INVERT_SHUFFLE)) + context.newBroadcastPendingIntent(PlaybackServiceFragment.ACTION_INVERT_SHUFFLE)) // Set up the repeat/shuffle buttons. When working with RemoteViews, we will // need to hard-code different accent tinting configurations, as stateful drawables diff --git a/build.gradle b/build.gradle index fdb033f43..eae63966b 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,7 @@ buildscript { } plugins { - id "com.android.application" version '8.2.0' apply false + id "com.android.application" version '8.2.1' apply false id "androidx.navigation.safeargs.kotlin" version "$navigation_version" apply false id "org.jetbrains.kotlin.android" version "$kotlin_version" apply false id "com.google.devtools.ksp" version '1.9.10-1.0.13' apply false