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:
parent
317c83a4d1
commit
22a22a883f
17 changed files with 229 additions and 210 deletions
|
@ -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" />
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
|
@ -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) {
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
|
@ -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) =
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
68
app/src/main/java/org/oxycblt/auxio/service/AuxioService.kt
Normal file
68
app/src/main/java/org/oxycblt/auxio/service/AuxioService.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue