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.
This commit is contained in:
Alexander Capehart 2024-02-25 11:26:20 -07:00
parent 317c83a4d1
commit 22a22a883f
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
17 changed files with 229 additions and 210 deletions

View file

@ -77,22 +77,12 @@
</intent-filter>
</activity>
<!--
Service handling querying the media database, extracting metadata, and constructing
the music library.
-->
<service
android:name=".music.system.IndexerService"
android:foregroundServiceType="dataSync"
android:icon="@mipmap/ic_launcher"
android:exported="false"
android:roundIcon="@mipmap/ic_launcher" />
<!--
Service handling music playback, system components, and state saving.
-->
<service
android:name=".playback.system.PlaybackService"
android:name=".service.AuxioService"
android:foregroundServiceType="mediaPlayback"
android:icon="@mipmap/ic_launcher"
android:exported="false"
@ -103,7 +93,7 @@
See the class for more info.
-->
<receiver
android:name=".playback.system.MediaButtonReceiver"
android:name=".playback.service.MediaButtonReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />

View file

@ -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.

View file

@ -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 {

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music.system
package org.oxycblt.auxio.music.service
import android.content.Context
import android.os.SystemClock

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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) {

View file

@ -16,11 +16,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.playback.system
package org.oxycblt.auxio.playback.service
import android.bluetooth.BluetoothProfile
import android.content.BroadcastReceiver

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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)
}
}

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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()

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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) =

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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()
}
}
}

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.playback.system
package org.oxycblt.auxio.playback.service
import android.content.Context
import androidx.media3.datasource.ContentDataSource

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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)
}
}
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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
}
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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()
}
}

View file

@ -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

View file

@ -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