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