From 8bc74188870aaa0df2e6a46f706dda29ec9b9281 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 18 May 2024 17:24:08 -0600 Subject: [PATCH] tasker: kind of working plugin --- app/src/main/AndroidManifest.xml | 19 +++++ .../java/org/oxycblt/auxio/AuxioService.kt | 20 ++--- .../java/org/oxycblt/auxio/MainActivity.kt | 4 +- .../service/ExoPlaybackStateHolder.kt | 16 +++- .../service/MediaSessionServiceFragment.kt | 21 +++++- .../playback/state/PlaybackStateHolder.kt | 11 ++- .../playback/state/PlaybackStateManager.kt | 15 ++++ .../org/oxycblt/auxio/tasker/RestoreState.kt | 75 +++++++++++++++++++ .../org/oxycblt/auxio/tasker/ShuffleAll.kt | 74 ++++++++++++++++++ .../java/org/oxycblt/auxio/tasker/Start.kt | 21 ++---- .../oxycblt/auxio/tasker/TaskerEntryPoint.kt | 30 ++++++++ 11 files changed, 269 insertions(+), 37 deletions(-) create mode 100644 app/src/main/java/org/oxycblt/auxio/tasker/RestoreState.kt create mode 100644 app/src/main/java/org/oxycblt/auxio/tasker/ShuffleAll.kt create mode 100644 app/src/main/java/org/oxycblt/auxio/tasker/TaskerEntryPoint.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d5c61525b..440314553 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -147,5 +147,24 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/org/oxycblt/auxio/AuxioService.kt b/app/src/main/java/org/oxycblt/auxio/AuxioService.kt index fd44f6a5b..29d7c6168 100644 --- a/app/src/main/java/org/oxycblt/auxio/AuxioService.kt +++ b/app/src/main/java/org/oxycblt/auxio/AuxioService.kt @@ -28,8 +28,7 @@ import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject import org.oxycblt.auxio.music.service.IndexerServiceFragment import org.oxycblt.auxio.playback.service.MediaSessionServiceFragment -import org.oxycblt.auxio.tasker.indicateServiceRunning -import org.oxycblt.auxio.tasker.indicateServiceStopped +import org.oxycblt.auxio.util.logD @AndroidEntryPoint class AuxioService : MediaLibraryService(), ForegroundListener { @@ -37,16 +36,17 @@ class AuxioService : MediaLibraryService(), ForegroundListener { @Inject lateinit var indexingFragment: IndexerServiceFragment + private var nativeStart = false + @SuppressLint("WrongConstant") override fun onCreate() { super.onCreate() mediaSessionFragment.attach(this, this) indexingFragment.attach(this) - indicateServiceRunning() } override fun onBind(intent: Intent?): IBinder? { - handleIntent(intent) + // handleIntent(intent) return super.onBind(intent) } @@ -58,7 +58,8 @@ class AuxioService : MediaLibraryService(), ForegroundListener { } private fun handleIntent(intent: Intent?) { - val nativeStart = intent?.getBooleanExtra(INTENT_KEY_NATIVE_START, false) ?: false + nativeStart = intent?.getBooleanExtra(INTENT_KEY_INTERNAL_START, false) ?: false + logD("${intent} $nativeStart") if (!nativeStart) { // Some foreign code started us, no guarantees about foreground stability. Figure // out what to do. @@ -73,7 +74,6 @@ class AuxioService : MediaLibraryService(), ForegroundListener { override fun onDestroy() { super.onDestroy() - indicateServiceStopped() indexingFragment.release() mediaSessionFragment.release() } @@ -86,7 +86,9 @@ class AuxioService : MediaLibraryService(), ForegroundListener { } override fun updateForeground(change: ForegroundListener.Change) { - if (mediaSessionFragment.hasNotification()) { + val state = mediaSessionFragment.hasNotification() + + if (state == MediaSessionServiceFragment.NotificationState.RUNNING) { if (change == ForegroundListener.Change.MEDIA_SESSION) { mediaSessionFragment.createNotification { startForeground(it.notificationId, it.notification) @@ -98,7 +100,7 @@ class AuxioService : MediaLibraryService(), ForegroundListener { indexingFragment.createNotification { if (it != null) { startForeground(it.code, it.build()) - } else { + } else if (state == MediaSessionServiceFragment.NotificationState.NOT_RUNNING) { ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) } } @@ -107,7 +109,7 @@ class AuxioService : MediaLibraryService(), ForegroundListener { companion object { // This is only meant for Auxio to internally ensure that it's state management will work. - const val INTENT_KEY_NATIVE_START = BuildConfig.APPLICATION_ID + ".service.NATIVE_START" + const val INTENT_KEY_INTERNAL_START = BuildConfig.APPLICATION_ID + ".service.INTERNAL_START" } } diff --git a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt index 42ad2a134..d62edfddb 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt @@ -71,11 +71,11 @@ class MainActivity : AppCompatActivity() { startService( Intent(this, AuxioService::class.java) - .putExtra(AuxioService.INTENT_KEY_NATIVE_START, true)) + .putExtra(AuxioService.INTENT_KEY_INTERNAL_START, true)) if (!startIntentAction(intent)) { // No intent action to do, just restore the previously saved state. - playbackModel.playDeferred(DeferredPlayback.RestoreState) + playbackModel.playDeferred(DeferredPlayback.RestoreState(sessionRequired = false)) } } 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 8413738b9..1fdfee063 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 @@ -85,7 +85,7 @@ class ExoPlaybackStateHolder( private var currentSaveJob: Job? = null private var openAudioEffectSession = false - var sessionOngoing = false + override var sessionOngoing = false private set fun attach() { @@ -157,16 +157,24 @@ class ExoPlaybackStateHolder( musicRepository.deviceLibrary // No library, cannot do anything. ?: return false - + logD((Exception().stackTraceToString())) when (action) { // Restore state -> Start a new restoreState job is DeferredPlayback.RestoreState -> { logD("Restoring playback state") restoreScope.launch { - persistenceRepository.readState()?.let { + val state = persistenceRepository.readState() + if (state != null) { // 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(it, false) } + withContext(Dispatchers.Main) { + if (action.sessionRequired) { + sessionOngoing = true + } + playbackManager.applySavedState(state, false) + } + } else if (action.sessionRequired) { + error("No playback state to restore, but need to start session") } } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionServiceFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionServiceFragment.kt index fb884cee3..e578da74d 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionServiceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/service/MediaSessionServiceFragment.kt @@ -51,7 +51,6 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.music.service.MediaItemBrowser import org.oxycblt.auxio.playback.state.DeferredPlayback import org.oxycblt.auxio.playback.state.PlaybackStateManager -import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.newMainPendingIntent class MediaSessionServiceFragment @@ -112,11 +111,25 @@ constructor( fun handleNonNativeStart() { // At minimum we want to ensure an active playback state. // TODO: Possibly also force to go foreground? - logD("Handling non-native start.") - playbackManager.playDeferred(DeferredPlayback.RestoreState) + // We assume that all non-native starts are from media controllers that should know + // what they are doing and have their own commands they want to execute. + playbackManager.playDeferred(DeferredPlayback.RestoreState(sessionRequired = true)) } - fun hasNotification(): Boolean = exoHolder.sessionOngoing + enum class NotificationState { + RUNNING, + NOT_RUNNING, + MAYBE_LATER + } + + fun hasNotification(): NotificationState = + if (exoHolder.sessionOngoing) { + NotificationState.RUNNING + } else if (playbackManager.hasDeferredPlayback()) { + NotificationState.MAYBE_LATER + } else { + NotificationState.NOT_RUNNING + } fun createNotification(post: (MediaNotification) -> Unit) { val notification = diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateHolder.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateHolder.kt index 857ac6898..6cd0c94ac 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateHolder.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateHolder.kt @@ -41,6 +41,9 @@ interface PlaybackStateHolder { /** The current [MusicParent] being played from. Null if playing from all songs. */ val parent: MusicParent? + /** Whether the player is in an active playback session. */ + val sessionOngoing: Boolean + /** * Resolve the current queue state as a [RawQueue]. * @@ -275,8 +278,12 @@ data class QueueChange(val type: Type, val instructions: UpdateInstructions) { /** Possible long-running background tasks handled by the background playback task. */ sealed interface DeferredPlayback { - /** Restore the previously saved playback state. */ - data object RestoreState : DeferredPlayback + /** + * Restore the previously saved playback state. + * + * @param sessionRequired Whether a playback session must be started after restoration. + */ + data class RestoreState(val sessionRequired: Boolean) : DeferredPlayback /** * Start shuffled playback of the entire music library. Analogous to the "Shuffle All" shortcut. 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 347b099ca..bd10eea95 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 @@ -66,6 +66,9 @@ interface PlaybackStateManager { /** Whether the queue is shuffled or not. */ val isShuffled: Boolean + /** Whether there is an ongoing playback session or not. */ + val sessionOngoing: Boolean + /** The audio session ID of the internal player. Null if no internal player exists. */ val currentAudioSessionId: Int? @@ -195,6 +198,13 @@ interface PlaybackStateManager { */ fun ack(stateHolder: PlaybackStateHolder, ack: StateAck) + /** + * Check if there is a pending [DeferredPlayback] to handle. + * + * @return Whether there is a pending [DeferredPlayback] to handle. + */ + fun hasDeferredPlayback(): Boolean + /** * Start a [DeferredPlayback] for the current [PlaybackStateHolder] to handle eventually. * @@ -382,6 +392,9 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { override val isShuffled get() = stateMirror.isShuffled + override val sessionOngoing + get() = stateHolder?.sessionOngoing ?: false + override val currentAudioSessionId: Int? get() = stateHolder?.audioSessionId @@ -522,6 +535,8 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager { // --- INTERNAL PLAYER FUNCTIONS --- + @Synchronized override fun hasDeferredPlayback(): Boolean = pendingDeferredPlayback != null + @Synchronized override fun playDeferred(action: DeferredPlayback) { val stateHolder = stateHolder diff --git a/app/src/main/java/org/oxycblt/auxio/tasker/RestoreState.kt b/app/src/main/java/org/oxycblt/auxio/tasker/RestoreState.kt new file mode 100644 index 000000000..dd64b7dd5 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/tasker/RestoreState.kt @@ -0,0 +1,75 @@ +/* + * 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 new file mode 100644 index 000000000..9ceae90ed --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/tasker/ShuffleAll.kt @@ -0,0 +1,74 @@ +/* + * 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 9e27486c6..82e1bb887 100644 --- a/app/src/main/java/org/oxycblt/auxio/tasker/Start.kt +++ b/app/src/main/java/org/oxycblt/auxio/tasker/Start.kt @@ -32,26 +32,13 @@ import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResult import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResultSucess import org.oxycblt.auxio.AuxioService -private var serviceRunning = false - -fun indicateServiceRunning() { - serviceRunning = true -} - -fun indicateServiceStopped() { - serviceRunning = false -} - class StartActionHelper(config: TaskerPluginConfig) : TaskerPluginConfigHelperNoOutputOrInput(config) { override val runnerClass: Class get() = StartActionRunner::class.java override fun addToStringBlurb(input: TaskerInput, blurbBuilder: StringBuilder) { - blurbBuilder.append( - "Starts the Auxio Service. This will block until the service is fully initialized." + - "You must start active playback/foreground state after this or Auxio may" + - "crash.") + blurbBuilder.append("Starts the Auxio Service. You MUST apply an action after this.") } } @@ -69,8 +56,10 @@ class StartConfigBasicAction : Activity(), TaskerPluginConfigNoInput { class StartActionRunner : TaskerPluginRunnerActionNoOutputOrInput() { override fun run(context: Context, input: TaskerInput): TaskerPluginResult { - ContextCompat.startForegroundService(context, Intent(context, AuxioService::class.java)) - while (!serviceRunning) {} + ContextCompat.startForegroundService( + context, + Intent(context, AuxioService::class.java) + .putExtra(AuxioService.INTENT_KEY_INTERNAL_START, true)) return TaskerPluginResultSucess() } } diff --git a/app/src/main/java/org/oxycblt/auxio/tasker/TaskerEntryPoint.kt b/app/src/main/java/org/oxycblt/auxio/tasker/TaskerEntryPoint.kt new file mode 100644 index 000000000..e4d28629c --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/tasker/TaskerEntryPoint.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2024 Auxio Project + * TaskerEntryPoint.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 dagger.hilt.EntryPoint +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import org.oxycblt.auxio.playback.state.PlaybackStateManager + +@EntryPoint +@InstallIn(SingletonComponent::class) +interface TaskerEntryPoint { + fun playbackManager(): PlaybackStateManager +}