diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 440314553..8fbbd234a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -136,32 +136,11 @@ - - - - - - - - - - - - - + android:label="Start"> diff --git a/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt b/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt index 86f3d1984..e3ed12787 100644 --- a/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt +++ b/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt @@ -57,6 +57,7 @@ object IntegerTable { const val PLAYBACK_NOTIFICATION_CODE = 0xA0A0 /** "Music loading" notification code */ const val INDEXER_NOTIFICATION_CODE = 0xA0A1 + const val TASKER_ERROR_NOT_RESTORED = 0xA0A2 /** MainActivity Intent request code */ const val REQUEST_CODE = 0xA0C0 /** RepeatMode.NONE */ diff --git a/app/src/main/java/org/oxycblt/auxio/music/service/IndexerServiceFragment.kt b/app/src/main/java/org/oxycblt/auxio/music/service/IndexerServiceFragment.kt index 6362def6b..1cd81d3ab 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/service/IndexerServiceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/service/IndexerServiceFragment.kt @@ -96,7 +96,9 @@ constructor( // Not observing and done loading, exit foreground. logD("Exiting foreground") post(observingNotification) - } else { + } else if (!playbackManager.awaitingDeferredPlayback) { + // Very possible we are done loading music and now need to avoid downtime + // as the player begins to load the playback state. post(null) } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt index 1fdfee063..3fc1edf46 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/ExoPlaybackStateHolder.kt @@ -42,6 +42,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.coroutines.yield +import org.oxycblt.auxio.ForegroundListener import org.oxycblt.auxio.image.ImageSettings import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicRepository @@ -84,11 +85,13 @@ class ExoPlaybackStateHolder( private val restoreScope = CoroutineScope(Dispatchers.IO + saveJob) private var currentSaveJob: Job? = null private var openAudioEffectSession = false + private var foregroundListener: ForegroundListener? = null override var sessionOngoing = false private set - fun attach() { + fun attach(foregroundListener: ForegroundListener) { + this.foregroundListener = foregroundListener imageSettings.registerListener(this) player.addListener(this) replayGainProcessor.attach() @@ -105,6 +108,7 @@ class ExoPlaybackStateHolder( replayGainProcessor.release() imageSettings.unregisterListener(this) player.release() + foregroundListener = null } override var parent: MusicParent? = null @@ -168,13 +172,15 @@ class ExoPlaybackStateHolder( // Apply the saved state on the main thread to prevent code expecting // state updates on the main thread from crashing. withContext(Dispatchers.Main) { + playbackManager.applySavedState(state, false) if (action.sessionRequired) { sessionOngoing = true + foregroundListener?.updateForeground(ForegroundListener.Change.MEDIA_SESSION) } - playbackManager.applySavedState(state, false) } - } else if (action.sessionRequired) { - error("No playback state to restore, but need to start session") + } else { + logD("No saved state to restore") + foregroundListener?.updateForeground(ForegroundListener.Change.MEDIA_SESSION) } } } @@ -189,11 +195,15 @@ class ExoPlaybackStateHolder( // 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(context, action.uri)?.let { song -> + val song = deviceLibrary.findSongForUri(context, action.uri) + if (song != null) { playbackManager.play( requireNotNull(commandFactory.song(song, ShuffleMode.IMPLICIT)) { "Invalid playback parameters" }) + } else { + logD("No song found for uri") + foregroundListener?.updateForeground(ForegroundListener.Change.MEDIA_SESSION) } } } @@ -373,13 +383,36 @@ class ExoPlaybackStateHolder( rawQueue: RawQueue, ack: StateAck.NewPlayback? ) { - this.parent = parent - player.setMediaItems(rawQueue.heap.map { it.toMediaItem(context, null) }) - if (rawQueue.isShuffled) { - player.shuffleModeEnabled = true - player.setShuffleOrder(BetterShuffleOrder(rawQueue.shuffledMapping.toIntArray())) - } else { - player.shuffleModeEnabled = false + var sendNewPlaybackEvent = false + var shouldSeek = false + if (this.parent != parent) { + this.parent = parent + sendNewPlaybackEvent = true + } + if (rawQueue != resolveQueue()) { + player.setMediaItems(rawQueue.heap.map { it.toMediaItem(context, null) }) + if (rawQueue.isShuffled) { + player.shuffleModeEnabled = true + player.setShuffleOrder(BetterShuffleOrder(rawQueue.shuffledMapping.toIntArray())) + } else { + player.shuffleModeEnabled = false + } + player.seekTo(rawQueue.heapIndex, C.TIME_UNSET) + player.prepare() + player.pause() + sendNewPlaybackEvent = true + shouldSeek = true + } + + repeatMode(repeatMode) + // Positions in milliseconds will drift during tight restores (i.e what the music loader + // does to sanitize the state), compare by seconds instead. +// if (positionMs.msToSecs() != player.currentPosition.msToSecs() || shouldSeek) { +// player.seekTo(positionMs) +// } + + if (sendNewPlaybackEvent) { + ack?.let { playbackManager.ack(this, it) } } player.seekTo(rawQueue.heapIndex, C.TIME_UNSET) player.prepare() diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt index bd10eea95..8273cb57a 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt @@ -69,6 +69,10 @@ interface PlaybackStateManager { /** Whether there is an ongoing playback session or not. */ val sessionOngoing: Boolean + /* Whether the player is awaiting a [DeferredPlayback] to be consumed. */ + val awaitingDeferredPlayback: Boolean + + /** The audio session ID of the internal player. Null if no internal player exists. */ val currentAudioSessionId: Int? @@ -395,6 +399,10 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { override val sessionOngoing get() = stateHolder?.sessionOngoing ?: false + override val awaitingDeferredPlayback: Boolean + get() = pendingDeferredPlayback != null + + override val currentAudioSessionId: Int? get() = stateHolder?.audioSessionId diff --git a/app/src/main/java/org/oxycblt/auxio/tasker/RestoreState.kt b/app/src/main/java/org/oxycblt/auxio/tasker/RestoreState.kt deleted file mode 100644 index dd64b7dd5..000000000 --- a/app/src/main/java/org/oxycblt/auxio/tasker/RestoreState.kt +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (c) 2024 Auxio Project - * RestoreState.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.tasker - -import android.app.Activity -import android.content.Context -import android.content.Intent -import android.os.Bundle -import androidx.core.content.ContextCompat -import com.joaomgcd.taskerpluginlibrary.action.TaskerPluginRunnerActionNoOutputOrInput -import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfig -import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfigHelperNoOutputOrInput -import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfigNoInput -import com.joaomgcd.taskerpluginlibrary.input.TaskerInput -import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResult -import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResultSucess -import dagger.hilt.EntryPoints -import kotlinx.coroutines.runBlocking -import org.oxycblt.auxio.AuxioService -import org.oxycblt.auxio.playback.state.DeferredPlayback - -class RestoreStateHelper(config: TaskerPluginConfig) : - TaskerPluginConfigHelperNoOutputOrInput(config) { - override val runnerClass: Class - get() = RestoreStateRunner::class.java - - override fun addToStringBlurb(input: TaskerInput, blurbBuilder: StringBuilder) { - blurbBuilder.append("Shuffles All Songs Once the Service is Available") - } -} - -class RestoreStateConfigBasicAction : Activity(), TaskerPluginConfigNoInput { - override val context: Context - get() = applicationContext - - private val taskerHelper by lazy { RestoreStateHelper(this) } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - taskerHelper.finishForTasker() - } -} - -class RestoreStateRunner : TaskerPluginRunnerActionNoOutputOrInput() { - override fun run(context: Context, input: TaskerInput): TaskerPluginResult { - ContextCompat.startForegroundService( - context, - Intent(context, AuxioService::class.java) - .putExtra(AuxioService.INTENT_KEY_INTERNAL_START, true)) - val entryPoint = EntryPoints.get(context.applicationContext, TaskerEntryPoint::class.java) - val playbackManager = entryPoint.playbackManager() - runBlocking { - playbackManager.playDeferred(DeferredPlayback.RestoreState(sessionRequired = true)) - } - while (!playbackManager.sessionOngoing) {} - Thread.sleep(100) - return TaskerPluginResultSucess() - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/tasker/ShuffleAll.kt b/app/src/main/java/org/oxycblt/auxio/tasker/ShuffleAll.kt deleted file mode 100644 index 9ceae90ed..000000000 --- a/app/src/main/java/org/oxycblt/auxio/tasker/ShuffleAll.kt +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (c) 2024 Auxio Project - * ShuffleAll.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.tasker - -import android.app.Activity -import android.content.Context -import android.content.Intent -import android.os.Bundle -import androidx.core.content.ContextCompat -import com.joaomgcd.taskerpluginlibrary.action.TaskerPluginRunnerActionNoOutputOrInput -import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfig -import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfigHelperNoOutputOrInput -import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfigNoInput -import com.joaomgcd.taskerpluginlibrary.input.TaskerInput -import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResult -import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResultSucess -import dagger.hilt.EntryPoints -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runBlocking -import org.oxycblt.auxio.AuxioService -import org.oxycblt.auxio.playback.state.DeferredPlayback - -class ShuffleAllHelper(config: TaskerPluginConfig) : - TaskerPluginConfigHelperNoOutputOrInput(config) { - override val runnerClass: Class - get() = ShuffleAllRunner::class.java - - override fun addToStringBlurb(input: TaskerInput, blurbBuilder: StringBuilder) { - blurbBuilder.append("Shuffles All Songs Once the Service is Available") - } -} - -class ShuffleAllConfigBasicAction : Activity(), TaskerPluginConfigNoInput { - override val context: Context - get() = applicationContext - - private val taskerHelper by lazy { ShuffleAllHelper(this) } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - taskerHelper.finishForTasker() - } -} - -class ShuffleAllRunner : TaskerPluginRunnerActionNoOutputOrInput() { - override fun run(context: Context, input: TaskerInput): TaskerPluginResult { - ContextCompat.startForegroundService( - context, - Intent(context, AuxioService::class.java) - .putExtra(AuxioService.INTENT_KEY_INTERNAL_START, true)) - val entryPoint = EntryPoints.get(context.applicationContext, TaskerEntryPoint::class.java) - val playbackManager = entryPoint.playbackManager() - runBlocking(Dispatchers.Main) { playbackManager.playDeferred(DeferredPlayback.ShuffleAll) } - while (!playbackManager.sessionOngoing) {} - Thread.sleep(100) - return TaskerPluginResultSucess() - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/tasker/Start.kt b/app/src/main/java/org/oxycblt/auxio/tasker/Start.kt index 82e1bb887..9de3661d1 100644 --- a/app/src/main/java/org/oxycblt/auxio/tasker/Start.kt +++ b/app/src/main/java/org/oxycblt/auxio/tasker/Start.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - + package org.oxycblt.auxio.tasker import android.app.Activity @@ -29,16 +29,21 @@ import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfigHelperNoOutputO import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfigNoInput import com.joaomgcd.taskerpluginlibrary.input.TaskerInput import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResult +import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResultError import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResultSucess +import dagger.hilt.EntryPoints +import kotlinx.coroutines.runBlocking import org.oxycblt.auxio.AuxioService +import org.oxycblt.auxio.IntegerTable +import org.oxycblt.auxio.playback.state.DeferredPlayback -class StartActionHelper(config: TaskerPluginConfig) : - TaskerPluginConfigHelperNoOutputOrInput(config) { - override val runnerClass: Class - get() = StartActionRunner::class.java +class StartHelper(config: TaskerPluginConfig) : + TaskerPluginConfigHelperNoOutputOrInput(config) { + override val runnerClass: Class + get() = StartStateRunner::class.java override fun addToStringBlurb(input: TaskerInput, blurbBuilder: StringBuilder) { - blurbBuilder.append("Starts the Auxio Service. You MUST apply an action after this.") + blurbBuilder.append("Shuffles All Songs Once the Service is Available") } } @@ -46,7 +51,7 @@ class StartConfigBasicAction : Activity(), TaskerPluginConfigNoInput { override val context: Context get() = applicationContext - private val taskerHelper by lazy { StartActionHelper(this) } + private val taskerHelper by lazy { StartHelper(this) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -54,12 +59,27 @@ class StartConfigBasicAction : Activity(), TaskerPluginConfigNoInput { } } -class StartActionRunner : TaskerPluginRunnerActionNoOutputOrInput() { +class StartStateRunner : TaskerPluginRunnerActionNoOutputOrInput() { override fun run(context: Context, input: TaskerInput): TaskerPluginResult { ContextCompat.startForegroundService( context, Intent(context, AuxioService::class.java) - .putExtra(AuxioService.INTENT_KEY_INTERNAL_START, true)) + .putExtra(AuxioService.INTENT_KEY_INTERNAL_START, true) + ) + val entryPoint = EntryPoints.get(context.applicationContext, TaskerEntryPoint::class.java) + val playbackManager = entryPoint.playbackManager() + runBlocking { + playbackManager.playDeferred(DeferredPlayback.RestoreState(sessionRequired = true)) + } + while (!playbackManager.sessionOngoing) { + if (!playbackManager.awaitingDeferredPlayback) { + return TaskerPluginResultError( + IntegerTable.TASKER_ERROR_NOT_RESTORED, + "No state to restore, did not restart playback." + ) + } + } + Thread.sleep(100) return TaskerPluginResultSucess() } } diff --git a/media b/media index 00124cbac..9fc2401b8 160000 --- a/media +++ b/media @@ -1 +1 @@ -Subproject commit 00124cbac493c06a24e19b01893946bdaf8faf58 +Subproject commit 9fc2401b8fdc2b23905402462e775c6db4e1527f