From 2617f50359674992283039a40056d4f3a14b11a5 Mon Sep 17 00:00:00 2001 From: rebel onion <87634197+rebelonion@users.noreply.github.com> Date: Sun, 17 Nov 2024 20:59:08 -0600 Subject: [PATCH 01/18] feat: controller extensions --- .../dev/brahmkshatriya/echo/PlayerService.kt | 5 + .../brahmkshatriya/echo/di/ExtensionModule.kt | 7 + .../echo/extensions/ExtensionLoader.kt | 23 +++ .../echo/extensions/ExtensionRepo.kt | 10 + .../playback/listeners/ControllerListener.kt | 191 ++++++++++++++++++ .../ui/extension/ExtensionInfoFragment.kt | 2 + .../ExtensionInstallerBottomSheet.kt | 1 + .../ui/extension/ExtensionsListBottomSheet.kt | 1 + .../echo/ui/login/LoginFragment.kt | 1 + .../echo/ui/login/LoginViewModel.kt | 2 + .../echo/ui/settings/ExtensionFragment.kt | 1 + .../echo/viewmodels/ExtensionViewModel.kt | 3 + .../res/layout/fragment_manage_extensions.xml | 5 + app/src/main/res/values/strings.xml | 1 + .../brahmkshatriya/echo/common/Extension.kt | 8 +- .../echo/common/clients/ControllerClient.kt | 24 +++ .../echo/common/helpers/ExtensionType.kt | 3 +- .../providers/ControllerClientsProvider.kt | 8 + 18 files changed, 294 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/dev/brahmkshatriya/echo/playback/listeners/ControllerListener.kt create mode 100644 common/src/main/java/dev/brahmkshatriya/echo/common/clients/ControllerClient.kt create mode 100644 common/src/main/java/dev/brahmkshatriya/echo/common/providers/ControllerClientsProvider.kt diff --git a/app/src/main/java/dev/brahmkshatriya/echo/PlayerService.kt b/app/src/main/java/dev/brahmkshatriya/echo/PlayerService.kt index f3734066..ab6f1373 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/PlayerService.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/PlayerService.kt @@ -23,6 +23,7 @@ import dev.brahmkshatriya.echo.playback.Current import dev.brahmkshatriya.echo.playback.PlayerCallback import dev.brahmkshatriya.echo.playback.ResumptionUtils import dev.brahmkshatriya.echo.playback.listeners.AudioFocusListener +import dev.brahmkshatriya.echo.playback.listeners.ControllerListener import dev.brahmkshatriya.echo.playback.listeners.PlayerEventListener import dev.brahmkshatriya.echo.playback.listeners.Radio import dev.brahmkshatriya.echo.playback.listeners.TrackingListener @@ -115,6 +116,7 @@ class PlayerService : MediaLibraryService() { val extListFlow = extensionLoader.extensions val extFlow = extensionLoader.current val trackerList = extensionLoader.trackers + val controllerList = extensionLoader.controllers val exoPlayer = createExoplayer(extListFlow) exoPlayer.prepare() @@ -149,6 +151,9 @@ class PlayerService : MediaLibraryService() { exoPlayer.addListener( TrackingListener(exoPlayer, scope, extListFlow, trackerList, throwFlow) ) + exoPlayer.addListener( + ControllerListener(exoPlayer, scope, controllerList, throwFlow) + ) settings.registerOnSharedPreferenceChangeListener { prefs, key -> when (key) { SKIP_SILENCE -> exoPlayer.skipSilenceEnabled = prefs.getBoolean(key, true) diff --git a/app/src/main/java/dev/brahmkshatriya/echo/di/ExtensionModule.kt b/app/src/main/java/dev/brahmkshatriya/echo/di/ExtensionModule.kt index 651a687d..fd6b4187 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/di/ExtensionModule.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/di/ExtensionModule.kt @@ -10,6 +10,7 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import dev.brahmkshatriya.echo.EchoDatabase +import dev.brahmkshatriya.echo.common.ControllerExtension import dev.brahmkshatriya.echo.common.LyricsExtension import dev.brahmkshatriya.echo.common.MusicExtension import dev.brahmkshatriya.echo.common.TrackerExtension @@ -50,6 +51,10 @@ class ExtensionModule { @Singleton fun provideTrackerListFlow() = MutableStateFlow?>(null) + @Provides + @Singleton + fun provideControllerListFlow() = MutableStateFlow?>(null) + @Provides @Singleton fun provideExtensionLoader( @@ -62,6 +67,7 @@ class ExtensionModule { offlineExtension: OfflineExtension, extensionListFlow: MutableStateFlow?>, trackerListFlow: MutableStateFlow?>, + controllerListFlow: MutableStateFlow?>, lyricsListFlow: MutableStateFlow?>, extensionFlow: MutableStateFlow, ) = run { @@ -78,6 +84,7 @@ class ExtensionModule { userFlow, extensionListFlow, trackerListFlow, + controllerListFlow, lyricsListFlow, extensionFlow, ) diff --git a/app/src/main/java/dev/brahmkshatriya/echo/extensions/ExtensionLoader.kt b/app/src/main/java/dev/brahmkshatriya/echo/extensions/ExtensionLoader.kt index db7524bc..5ee4980a 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/extensions/ExtensionLoader.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/extensions/ExtensionLoader.kt @@ -2,6 +2,7 @@ package dev.brahmkshatriya.echo.extensions import android.content.Context import android.content.SharedPreferences +import dev.brahmkshatriya.echo.common.ControllerExtension import dev.brahmkshatriya.echo.common.Extension import dev.brahmkshatriya.echo.common.LyricsExtension import dev.brahmkshatriya.echo.common.MusicExtension @@ -10,6 +11,7 @@ import dev.brahmkshatriya.echo.common.clients.ExtensionClient import dev.brahmkshatriya.echo.common.clients.LoginClient import dev.brahmkshatriya.echo.common.helpers.ExtensionType import dev.brahmkshatriya.echo.common.models.Metadata +import dev.brahmkshatriya.echo.common.providers.ControllerClientsProvider import dev.brahmkshatriya.echo.common.providers.LyricsClientsProvider import dev.brahmkshatriya.echo.common.providers.MusicClientsProvider import dev.brahmkshatriya.echo.common.providers.TrackerClientsProvider @@ -52,6 +54,7 @@ class ExtensionLoader( private val userFlow: MutableSharedFlow, private val extensionListFlow: MutableStateFlow?>, private val trackerListFlow: MutableStateFlow?>, + private val controllerListFlow: MutableStateFlow?>, private val lyricsListFlow: MutableStateFlow?>, private val extensionFlow: MutableStateFlow, ) { @@ -62,9 +65,11 @@ class ExtensionLoader( private val musicExtensionRepo = MusicExtensionRepo(context, listener, fileListener, builtIn) private val trackerExtensionRepo = TrackerExtensionRepo(context, listener, fileListener) + private val controllerExtensionRepo = ControllerExtensionRepo(context, listener, fileListener) private val lyricsExtensionRepo = LyricsExtensionRepo(context, listener, fileListener) val trackers = trackerListFlow + val controllers = controllerListFlow val extensions = extensionListFlow val current = extensionFlow val currentWithUser = MutableStateFlow>(null to null) @@ -113,6 +118,7 @@ class ExtensionLoader( ) combined.collect { list -> val trackerExtensions = trackerListFlow.value.orEmpty() + val controllerExtensions = controllerListFlow.value.orEmpty() val lyricsExtensions = lyricsListFlow.value.orEmpty() val musicExtensions = extensionListFlow.value.orEmpty() list?.forEach { extension -> @@ -121,6 +127,11 @@ class ExtensionLoader( setTrackerExtensions(it) } } + extension.get(throwableFlow) { + inject(extension.name, requiredControllerClients, controllerExtensions) { + setControllerExtensions(it) + } + } extension.get(throwableFlow) { inject(extension.name, requiredLyricsClients, lyricsExtensions) { setLyricsExtensions(it) @@ -162,6 +173,7 @@ class ExtensionLoader( private suspend fun getAllPlugins(scope: CoroutineScope) { val trackers = MutableStateFlow(null) + val controllers = MutableStateFlow(null) val lyrics = MutableStateFlow(null) val music = MutableStateFlow(null) scope.launch { @@ -174,6 +186,16 @@ class ExtensionLoader( trackers.emit(Unit) } } + scope.launch { + controllerExtensionRepo.getPlugins { list -> + val controllerExtensions = list.map { (metadata, client) -> + ControllerExtension(metadata, client) + } + controllerListFlow.value = controllerExtensions + controllerExtensions.setExtensions() + controllers.emit(Unit) + } + } scope.launch { lyricsExtensionRepo.getPlugins { list -> val lyricsExtensions = list.map { (metadata, client) -> @@ -186,6 +208,7 @@ class ExtensionLoader( } lyrics.first { it != null } trackers.first { it != null } + controllers.first { it != null } scope.launch { musicExtensionRepo.getPlugins { list -> diff --git a/app/src/main/java/dev/brahmkshatriya/echo/extensions/ExtensionRepo.kt b/app/src/main/java/dev/brahmkshatriya/echo/extensions/ExtensionRepo.kt index a134690a..cf4b14c1 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/extensions/ExtensionRepo.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/extensions/ExtensionRepo.kt @@ -1,6 +1,7 @@ package dev.brahmkshatriya.echo.extensions import android.content.Context +import dev.brahmkshatriya.echo.common.clients.ControllerClient import dev.brahmkshatriya.echo.common.clients.ExtensionClient import dev.brahmkshatriya.echo.common.clients.LyricsClient import dev.brahmkshatriya.echo.common.clients.TrackerClient @@ -89,6 +90,15 @@ class TrackerExtensionRepo( override val type = ExtensionType.TRACKER } +class ControllerExtensionRepo( + context: Context, + listener: PackageChangeListener, + fileChangeListener: FileChangeListener, + vararg repo: LazyPluginRepo +) : ExtensionRepo(context, listener, fileChangeListener, *repo) { + override val type = ExtensionType.CONTROLLER +} + class LyricsExtensionRepo( context: Context, listener: PackageChangeListener, diff --git a/app/src/main/java/dev/brahmkshatriya/echo/playback/listeners/ControllerListener.kt b/app/src/main/java/dev/brahmkshatriya/echo/playback/listeners/ControllerListener.kt new file mode 100644 index 00000000..71c3cac9 --- /dev/null +++ b/app/src/main/java/dev/brahmkshatriya/echo/playback/listeners/ControllerListener.kt @@ -0,0 +1,191 @@ +package dev.brahmkshatriya.echo.playback.listeners + +import androidx.media3.common.MediaItem +import androidx.media3.common.Player +import androidx.media3.common.Tracks +import androidx.media3.common.util.UnstableApi +import dev.brahmkshatriya.echo.common.ControllerExtension +import dev.brahmkshatriya.echo.common.clients.ControllerClient +import dev.brahmkshatriya.echo.extensions.get +import dev.brahmkshatriya.echo.playback.MediaItemUtils.track +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch + +@UnstableApi +class ControllerListener( + player: Player, + private val scope: CoroutineScope, + private val controllerExtensions: MutableStateFlow?>, + private val throwableFlow: MutableSharedFlow +) : PlayerListener(player) { + + init { + scope.launch { + controllerExtensions.collect { extensions -> + extensions?.forEach { + launch { + registerController(it) + } + } + } + } + } + + private suspend fun registerController(extension: ControllerExtension) { + extension.get(throwableFlow) { + onPlayRequest = { + try { + player.play() + } catch (e: Exception) { + throwableFlow.emit(e) + } + } + onPauseRequest = { + try { + player.pause() + } catch (e: Exception) { + throwableFlow.emit(e) + } + } + onNextRequest = { + try { + player.seekToNextMediaItem() + } catch (e: Exception) { + throwableFlow.emit(e) + } + } + onPreviousRequest = { + try { + player.seekToPreviousMediaItem() + } catch (e: Exception) { + throwableFlow.emit(e) + } + } + onSeekRequest = { position -> + try { + player.seekTo(position.toLong()) + } catch (e: Exception) { + throwableFlow.emit(e) + } + } + onMovePlaylistItemRequest = { fromIndex, toIndex -> + try { + player.moveMediaItem(fromIndex, toIndex) + } catch (e: Exception) { + throwableFlow.emit(e) + } + } + onRemovePlaylistItemRequest = { index -> + try { + player.removeMediaItem(index) + } catch (e: Exception) { + throwableFlow.emit(e) + } + } + onShuffleModeRequest = { enabled -> + try { + player.shuffleModeEnabled = enabled + } catch (e: Exception) { + throwableFlow.emit(e) + } + } + onRepeatModeRequest = { repeatMode -> + try { + player.repeatMode = repeatMode + } catch (e: Exception) { + throwableFlow.emit(e) + } + } + onVolumeRequest = { volume -> + try { + player.volume = volume.toFloat() + } catch (e: Exception) { + throwableFlow.emit(e) + } + } + } + + } + + private fun notifyControllers(block: suspend ControllerClient.() -> Unit) { + val controllers = controllerExtensions.value?.filter { it.metadata.enabled } ?: emptyList() + scope.launch { + controllers.forEach { + launch { + it.get(throwableFlow) { block() } + } + } + } + } + + override fun onTrackStart(mediaItem: MediaItem) { + val isPlaying = player.isPlaying + val position = player.currentPosition.toDouble() + val track = mediaItem.track + + notifyControllers { + onPlaybackStateChanged(isPlaying, position, track) + } + } + + override fun onTracksChanged(tracks: Tracks) { + super.onTracksChanged(tracks) + val playlist = List(player.mediaItemCount) { index -> + player.getMediaItemAt(index).track + } + + notifyControllers { + onPlaylistChanged(playlist) + } + } + + override fun onIsPlayingChanged(isPlaying: Boolean) { + super.onIsPlayingChanged(isPlaying) + val position = player.currentPosition.toDouble() + val track = player.currentMediaItem?.track ?: return + + notifyControllers { + onPlaybackStateChanged(isPlaying, position, track) + } + } + + override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) { + super.onShuffleModeEnabledChanged(shuffleModeEnabled) + val repeatMode = player.repeatMode + + notifyControllers { + onPlaybackModeChanged(shuffleModeEnabled, repeatMode) + } + } + + override fun onRepeatModeChanged(repeatMode: Int) { + super.onRepeatModeChanged(repeatMode) + val shuffleModeEnabled = player.shuffleModeEnabled + + notifyControllers { + onPlaybackModeChanged(shuffleModeEnabled, repeatMode) + } + } + + override fun onPositionDiscontinuity( + oldPosition: Player.PositionInfo, + newPosition: Player.PositionInfo, + reason: Int + ) { + super.onPositionDiscontinuity(oldPosition, newPosition, reason) + val position = newPosition.positionMs.toDouble() + + notifyControllers { + onPositionChanged(position) + } + } + + override fun onVolumeChanged(volume: Float) { + super.onVolumeChanged(volume) + notifyControllers { + onVolumeChanged(player.volume.toDouble()) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionInfoFragment.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionInfoFragment.kt index 431b8d01..d4a0fd52 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionInfoFragment.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionInfoFragment.kt @@ -88,6 +88,7 @@ class ExtensionInfoFragment : Fragment() { ExtensionType.MUSIC -> viewModel.extensionListFlow.getExtension(clientId) ExtensionType.TRACKER -> viewModel.trackerListFlow.getExtension(clientId) ExtensionType.LYRICS -> viewModel.lyricsListFlow.getExtension(clientId) + ExtensionType.CONTROLLER -> viewModel.controllerListFlow.getExtension(clientId) } if (extension == null) { @@ -127,6 +128,7 @@ class ExtensionInfoFragment : Fragment() { ExtensionType.MUSIC -> R.string.music ExtensionType.TRACKER -> R.string.tracker ExtensionType.LYRICS -> R.string.lyrics + ExtensionType.CONTROLLER -> R.string.controller } val typeString = getString(R.string.name_extension, getString(type)) binding.extensionDescription.text = "$typeString\n\n${metadata.description}\n\n$byAuthor" diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionInstallerBottomSheet.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionInstallerBottomSheet.kt index b077b1e3..c42e5c52 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionInstallerBottomSheet.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionInstallerBottomSheet.kt @@ -94,6 +94,7 @@ class ExtensionInstallerBottomSheet : BottomSheetDialogFragment() { ExtensionType.MUSIC -> R.string.music ExtensionType.TRACKER -> R.string.tracker ExtensionType.LYRICS -> R.string.lyrics + ExtensionType.CONTROLLER -> R.string.controller } val typeString = getString(R.string.name_extension, getString(type)) binding.extensionDescription.text = "$typeString\n\n${metadata.description}\n\n$byAuthor" diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionsListBottomSheet.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionsListBottomSheet.kt index abf0f49d..c6a1db49 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionsListBottomSheet.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionsListBottomSheet.kt @@ -55,6 +55,7 @@ class ExtensionsListBottomSheet : BottomSheetDialogFragment() { ExtensionType.LYRICS -> activityViewModels().value ExtensionType.MUSIC -> activityViewModels().value ExtensionType.TRACKER -> throw IllegalStateException("Tracker not supported") + ExtensionType.CONTROLLER -> throw IllegalStateException("Controller not supported") } val listener = object : OnButtonCheckedListener { diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/login/LoginFragment.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/login/LoginFragment.kt index 277c310b..c2fdec7f 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/login/LoginFragment.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/login/LoginFragment.kt @@ -112,6 +112,7 @@ class LoginFragment : Fragment() { ExtensionType.MUSIC -> loginViewModel.extensionList.getExtension(clientId) ExtensionType.TRACKER -> loginViewModel.trackerList.getExtension(clientId) ExtensionType.LYRICS -> loginViewModel.lyricsList.getExtension(clientId) + ExtensionType.CONTROLLER -> loginViewModel.controllerList.getExtension(clientId) } if (extension == null) { diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/login/LoginViewModel.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/login/LoginViewModel.kt index eff1b0f1..f7458272 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/login/LoginViewModel.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/login/LoginViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import dev.brahmkshatriya.echo.EchoDatabase import dev.brahmkshatriya.echo.R +import dev.brahmkshatriya.echo.common.ControllerExtension import dev.brahmkshatriya.echo.common.Extension import dev.brahmkshatriya.echo.common.LyricsExtension import dev.brahmkshatriya.echo.common.MusicExtension @@ -30,6 +31,7 @@ class LoginViewModel @Inject constructor( val extensionList: MutableStateFlow?>, val trackerList: MutableStateFlow?>, val lyricsList: MutableStateFlow?>, + val controllerList: MutableStateFlow?>, private val context: Application, val messageFlow: MutableSharedFlow, database: EchoDatabase, diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/settings/ExtensionFragment.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/settings/ExtensionFragment.kt index 6379b952..8ee449b1 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/settings/ExtensionFragment.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/settings/ExtensionFragment.kt @@ -92,6 +92,7 @@ class ExtensionFragment : BaseSettingsFragment() { ExtensionType.MUSIC -> extensionListFlow.getExtension(extensionId) ExtensionType.TRACKER -> trackerListFlow.getExtension(extensionId) ExtensionType.LYRICS -> lyricsListFlow.getExtension(extensionId) + ExtensionType.CONTROLLER -> controllerListFlow.getExtension(extensionId) } viewModelScope.launch { client?.run(throwableFlow) { diff --git a/app/src/main/java/dev/brahmkshatriya/echo/viewmodels/ExtensionViewModel.kt b/app/src/main/java/dev/brahmkshatriya/echo/viewmodels/ExtensionViewModel.kt index bced1763..d6ae1c6c 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/viewmodels/ExtensionViewModel.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/viewmodels/ExtensionViewModel.kt @@ -14,6 +14,7 @@ import dev.brahmkshatriya.echo.EchoDatabase import dev.brahmkshatriya.echo.ExtensionOpenerActivity import dev.brahmkshatriya.echo.ExtensionOpenerActivity.Companion.installExtension import dev.brahmkshatriya.echo.R +import dev.brahmkshatriya.echo.common.ControllerExtension import dev.brahmkshatriya.echo.common.Extension import dev.brahmkshatriya.echo.common.LyricsExtension import dev.brahmkshatriya.echo.common.MusicExtension @@ -62,6 +63,7 @@ class ExtensionViewModel @Inject constructor( val extensionListFlow: MutableStateFlow?>, val trackerListFlow: MutableStateFlow?>, val lyricsListFlow: MutableStateFlow?>, + val controllerListFlow: MutableStateFlow?>, val extensionFlow: MutableStateFlow, val settings: SharedPreferences, val database: EchoDatabase, @@ -130,6 +132,7 @@ class ExtensionViewModel @Inject constructor( ExtensionType.MUSIC -> extensionListFlow ExtensionType.TRACKER -> trackerListFlow ExtensionType.LYRICS -> lyricsListFlow + ExtensionType.CONTROLLER -> controllerListFlow } fun moveExtensionItem(type: ExtensionType, toPos: Int, fromPos: Int) { diff --git a/app/src/main/res/layout/fragment_manage_extensions.xml b/app/src/main/res/layout/fragment_manage_extensions.xml index eb694d00..d9dd4200 100644 --- a/app/src/main/res/layout/fragment_manage_extensions.xml +++ b/app/src/main/res/layout/fragment_manage_extensions.xml @@ -44,6 +44,11 @@ android:layout_height="wrap_content" android:text="@string/lyrics" /> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9b661690..287ce9a8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -267,6 +267,7 @@ Sources Backgrounds Source Selection + Controller Highest Medium diff --git a/common/src/main/java/dev/brahmkshatriya/echo/common/Extension.kt b/common/src/main/java/dev/brahmkshatriya/echo/common/Extension.kt index 7e4656cc..f58ef22b 100644 --- a/common/src/main/java/dev/brahmkshatriya/echo/common/Extension.kt +++ b/common/src/main/java/dev/brahmkshatriya/echo/common/Extension.kt @@ -1,5 +1,6 @@ package dev.brahmkshatriya.echo.common +import dev.brahmkshatriya.echo.common.clients.ControllerClient import dev.brahmkshatriya.echo.common.clients.ExtensionClient import dev.brahmkshatriya.echo.common.clients.LyricsClient import dev.brahmkshatriya.echo.common.clients.TrackerClient @@ -29,4 +30,9 @@ data class TrackerExtension( data class LyricsExtension( override val metadata: Metadata, override val instance: Lazy>, -) : Extension(ExtensionType.LYRICS, metadata, instance) \ No newline at end of file +) : Extension(ExtensionType.LYRICS, metadata, instance) + +data class ControllerExtension( + override val metadata: Metadata, + override val instance: Lazy>, +) : Extension(ExtensionType.CONTROLLER, metadata, instance) \ No newline at end of file diff --git a/common/src/main/java/dev/brahmkshatriya/echo/common/clients/ControllerClient.kt b/common/src/main/java/dev/brahmkshatriya/echo/common/clients/ControllerClient.kt new file mode 100644 index 00000000..342a8fb3 --- /dev/null +++ b/common/src/main/java/dev/brahmkshatriya/echo/common/clients/ControllerClient.kt @@ -0,0 +1,24 @@ +package dev.brahmkshatriya.echo.common.clients + +import dev.brahmkshatriya.echo.common.models.Track + +interface ControllerClient : ExtensionClient { + // app -> controller + suspend fun onPlaybackStateChanged(isPlaying: Boolean, position: Double, track: Track) + suspend fun onPlaylistChanged(playlist: List) + suspend fun onPlaybackModeChanged(isShuffle: Boolean, repeatState: Int) + suspend fun onPositionChanged(position: Double) + suspend fun onVolumeChanged(volume: Double) + + // controller -> app + var onPlayRequest: (suspend () -> Unit)? + var onPauseRequest: (suspend () -> Unit)? + var onNextRequest: (suspend () -> Unit)? + var onPreviousRequest: (suspend () -> Unit)? + var onSeekRequest: (suspend (position: Double) -> Unit)? + var onMovePlaylistItemRequest: (suspend (fromIndex: Int, toIndex: Int) -> Unit)? + var onRemovePlaylistItemRequest: (suspend (index: Int) -> Unit)? + var onShuffleModeRequest: (suspend (enabled: Boolean) -> Unit)? + var onRepeatModeRequest: (suspend (repeatMode: Int) -> Unit)? + var onVolumeRequest: (suspend (volume: Double) -> Unit)? +} \ No newline at end of file diff --git a/common/src/main/java/dev/brahmkshatriya/echo/common/helpers/ExtensionType.kt b/common/src/main/java/dev/brahmkshatriya/echo/common/helpers/ExtensionType.kt index 3275036a..c67859c9 100644 --- a/common/src/main/java/dev/brahmkshatriya/echo/common/helpers/ExtensionType.kt +++ b/common/src/main/java/dev/brahmkshatriya/echo/common/helpers/ExtensionType.kt @@ -3,5 +3,6 @@ package dev.brahmkshatriya.echo.common.helpers enum class ExtensionType(val feature: String) { MUSIC("music"), TRACKER("tracker"), - LYRICS("lyrics") + LYRICS("lyrics"), + CONTROLLER("controller") } \ No newline at end of file diff --git a/common/src/main/java/dev/brahmkshatriya/echo/common/providers/ControllerClientsProvider.kt b/common/src/main/java/dev/brahmkshatriya/echo/common/providers/ControllerClientsProvider.kt new file mode 100644 index 00000000..95535266 --- /dev/null +++ b/common/src/main/java/dev/brahmkshatriya/echo/common/providers/ControllerClientsProvider.kt @@ -0,0 +1,8 @@ +package dev.brahmkshatriya.echo.common.providers + +import dev.brahmkshatriya.echo.common.ControllerExtension + +interface ControllerClientsProvider { + val requiredControllerClients: List + fun setControllerExtensions(controllerClients: List) +} \ No newline at end of file From aa736c33a147075c3fca637a5e93a1050f1671e7 Mon Sep 17 00:00:00 2001 From: rebel onion <87634197+rebelonion@users.noreply.github.com> Date: Mon, 18 Nov 2024 01:37:15 -0600 Subject: [PATCH 02/18] fix: correct thread + volume listening --- .../dev/brahmkshatriya/echo/PlayerService.kt | 1 + .../playback/listeners/ControllerListener.kt | 75 +++++++++++-------- 2 files changed, 43 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/dev/brahmkshatriya/echo/PlayerService.kt b/app/src/main/java/dev/brahmkshatriya/echo/PlayerService.kt index ab6f1373..9125cc0a 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/PlayerService.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/PlayerService.kt @@ -99,6 +99,7 @@ class PlayerService : MediaLibraryService() { .setWakeMode(C.WAKE_MODE_NETWORK) .setSkipSilenceEnabled(settings.getBoolean(SKIP_SILENCE, true)) .setAudioAttributes(audioAttributes, true) + .setDeviceVolumeControlEnabled(true) .build() .also { it.trackSelectionParameters = it.trackSelectionParameters diff --git a/app/src/main/java/dev/brahmkshatriya/echo/playback/listeners/ControllerListener.kt b/app/src/main/java/dev/brahmkshatriya/echo/playback/listeners/ControllerListener.kt index 71c3cac9..75cf2903 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/playback/listeners/ControllerListener.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/playback/listeners/ControllerListener.kt @@ -2,6 +2,7 @@ package dev.brahmkshatriya.echo.playback.listeners import androidx.media3.common.MediaItem import androidx.media3.common.Player +import androidx.media3.common.Timeline import androidx.media3.common.Tracks import androidx.media3.common.util.UnstableApi import dev.brahmkshatriya.echo.common.ControllerExtension @@ -9,9 +10,11 @@ import dev.brahmkshatriya.echo.common.clients.ControllerClient import dev.brahmkshatriya.echo.extensions.get import dev.brahmkshatriya.echo.playback.MediaItemUtils.track import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext @UnstableApi class ControllerListener( @@ -36,79 +39,72 @@ class ControllerListener( private suspend fun registerController(extension: ControllerExtension) { extension.get(throwableFlow) { onPlayRequest = { - try { + tryOnMain(throwableFlow) { player.play() - } catch (e: Exception) { - throwableFlow.emit(e) } } onPauseRequest = { - try { + tryOnMain(throwableFlow) { player.pause() - } catch (e: Exception) { - throwableFlow.emit(e) } } onNextRequest = { - try { + tryOnMain(throwableFlow) { player.seekToNextMediaItem() - } catch (e: Exception) { - throwableFlow.emit(e) } } onPreviousRequest = { - try { + tryOnMain(throwableFlow) { player.seekToPreviousMediaItem() - } catch (e: Exception) { - throwableFlow.emit(e) } } onSeekRequest = { position -> - try { + tryOnMain(throwableFlow) { player.seekTo(position.toLong()) - } catch (e: Exception) { - throwableFlow.emit(e) } } onMovePlaylistItemRequest = { fromIndex, toIndex -> - try { + tryOnMain(throwableFlow) { player.moveMediaItem(fromIndex, toIndex) - } catch (e: Exception) { - throwableFlow.emit(e) } } onRemovePlaylistItemRequest = { index -> - try { + tryOnMain(throwableFlow) { player.removeMediaItem(index) - } catch (e: Exception) { - throwableFlow.emit(e) } } onShuffleModeRequest = { enabled -> - try { + tryOnMain(throwableFlow) { player.shuffleModeEnabled = enabled - } catch (e: Exception) { - throwableFlow.emit(e) } } onRepeatModeRequest = { repeatMode -> - try { + tryOnMain(throwableFlow) { player.repeatMode = repeatMode - } catch (e: Exception) { - throwableFlow.emit(e) } } onVolumeRequest = { volume -> - try { + tryOnMain(throwableFlow) { player.volume = volume.toFloat() - } catch (e: Exception) { - throwableFlow.emit(e) } } } } + private suspend fun ControllerClient.tryOnMain( + flow: MutableSharedFlow, + block: suspend ControllerClient.() -> Unit + ) { + withContext(Dispatchers.Main) { + try { + block() + } catch (e: Exception) { + flow.emit(e) + } + } + } + private fun notifyControllers(block: suspend ControllerClient.() -> Unit) { val controllers = controllerExtensions.value?.filter { it.metadata.enabled } ?: emptyList() scope.launch { @@ -132,6 +128,15 @@ class ControllerListener( override fun onTracksChanged(tracks: Tracks) { super.onTracksChanged(tracks) + updatePlaylist() + } + + override fun onTimelineChanged(timeline: Timeline, reason: Int) { + super.onTimelineChanged(timeline, reason) + updatePlaylist() + } + + private fun updatePlaylist() { val playlist = List(player.mediaItemCount) { index -> player.getMediaItemAt(index).track } @@ -141,6 +146,10 @@ class ControllerListener( } } + override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { + super.onMediaItemTransition(mediaItem, reason) + } + override fun onIsPlayingChanged(isPlaying: Boolean) { super.onIsPlayingChanged(isPlaying) val position = player.currentPosition.toDouble() @@ -182,10 +191,10 @@ class ControllerListener( } } - override fun onVolumeChanged(volume: Float) { - super.onVolumeChanged(volume) + override fun onDeviceVolumeChanged(volume: Int, muted: Boolean) { + super.onDeviceVolumeChanged(volume, muted) notifyControllers { - onVolumeChanged(player.volume.toDouble()) + onVolumeChanged(volume.toDouble()) } } } \ No newline at end of file From 38cc213936815e1b9383f53696bbe1939ea8eaa5 Mon Sep 17 00:00:00 2001 From: rebel onion <87634197+rebelonion@users.noreply.github.com> Date: Mon, 18 Nov 2024 17:37:58 -0600 Subject: [PATCH 03/18] fix: foreground exception --- .../dev/brahmkshatriya/echo/PlayerService.kt | 9 ++++- .../playback/listeners/ControllerListener.kt | 40 ++++++++++++------- 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/dev/brahmkshatriya/echo/PlayerService.kt b/app/src/main/java/dev/brahmkshatriya/echo/PlayerService.kt index 9125cc0a..b4cc84cc 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/PlayerService.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/PlayerService.kt @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import android.app.PendingIntent import android.content.Intent import android.content.SharedPreferences +import android.media.AudioManager import androidx.annotation.OptIn import androidx.media3.common.AudioAttributes import androidx.media3.common.C @@ -153,7 +154,13 @@ class PlayerService : MediaLibraryService() { TrackingListener(exoPlayer, scope, extListFlow, trackerList, throwFlow) ) exoPlayer.addListener( - ControllerListener(exoPlayer, scope, controllerList, throwFlow) + ControllerListener( + exoPlayer, + getSystemService(AUDIO_SERVICE) as AudioManager, + scope, + controllerList, + throwFlow + ) ) settings.registerOnSharedPreferenceChangeListener { prefs, key -> when (key) { diff --git a/app/src/main/java/dev/brahmkshatriya/echo/playback/listeners/ControllerListener.kt b/app/src/main/java/dev/brahmkshatriya/echo/playback/listeners/ControllerListener.kt index 75cf2903..e4d391a2 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/playback/listeners/ControllerListener.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/playback/listeners/ControllerListener.kt @@ -1,5 +1,6 @@ package dev.brahmkshatriya.echo.playback.listeners +import android.media.AudioManager import androidx.media3.common.MediaItem import androidx.media3.common.Player import androidx.media3.common.Timeline @@ -19,6 +20,7 @@ import kotlinx.coroutines.withContext @UnstableApi class ControllerListener( player: Player, + private val audioManager: AudioManager, private val scope: CoroutineScope, private val controllerExtensions: MutableStateFlow?>, private val throwableFlow: MutableSharedFlow @@ -39,53 +41,55 @@ class ControllerListener( private suspend fun registerController(extension: ControllerExtension) { extension.get(throwableFlow) { onPlayRequest = { - tryOnMain(throwableFlow) { + tryOnMain { player.play() } } onPauseRequest = { - tryOnMain(throwableFlow) { + tryOnMain { player.pause() } } onNextRequest = { - tryOnMain(throwableFlow) { + tryOnMain { player.seekToNextMediaItem() } } onPreviousRequest = { - tryOnMain(throwableFlow) { + tryOnMain { player.seekToPreviousMediaItem() } } onSeekRequest = { position -> - tryOnMain(throwableFlow) { + tryOnMain { player.seekTo(position.toLong()) } } onMovePlaylistItemRequest = { fromIndex, toIndex -> - tryOnMain(throwableFlow) { + tryOnMain { player.moveMediaItem(fromIndex, toIndex) } } onRemovePlaylistItemRequest = { index -> - tryOnMain(throwableFlow) { + tryOnMain { player.removeMediaItem(index) } } onShuffleModeRequest = { enabled -> - tryOnMain(throwableFlow) { + tryOnMain { player.shuffleModeEnabled = enabled } } onRepeatModeRequest = { repeatMode -> - tryOnMain(throwableFlow) { + tryOnMain { player.repeatMode = repeatMode } } onVolumeRequest = { volume -> - tryOnMain(throwableFlow) { - player.volume = volume.toFloat() + tryOnMain { + val denormalized = + volume * audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) + audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, denormalized.toInt(), 0) } } } @@ -93,14 +97,18 @@ class ControllerListener( } private suspend fun ControllerClient.tryOnMain( - flow: MutableSharedFlow, block: suspend ControllerClient.() -> Unit ) { - withContext(Dispatchers.Main) { + withContext(Dispatchers.Main.immediate) { try { + if (player.playbackState == Player.STATE_IDLE || + player.playbackState == Player.STATE_ENDED || + player.isPlaying.not()) { + return@withContext + } block() } catch (e: Exception) { - flow.emit(e) + throwableFlow.emit(e) } } } @@ -193,8 +201,10 @@ class ControllerListener( override fun onDeviceVolumeChanged(volume: Int, muted: Boolean) { super.onDeviceVolumeChanged(volume, muted) + val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) + val normalized = volume.toDouble() / maxVolume.toDouble() notifyControllers { - onVolumeChanged(volume.toDouble()) + onVolumeChanged(normalized) } } } \ No newline at end of file From 5daf27f7a97f6924e1280634e4741a30ba8c0c15 Mon Sep 17 00:00:00 2001 From: rebel onion <87634197+rebelonion@users.noreply.github.com> Date: Tue, 19 Nov 2024 18:26:01 -0600 Subject: [PATCH 04/18] fix: better service status checking --- .../dev/brahmkshatriya/echo/PlayerService.kt | 4 +- .../playback/listeners/ControllerListener.kt | 59 ++++++++++++++----- 2 files changed, 46 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/dev/brahmkshatriya/echo/PlayerService.kt b/app/src/main/java/dev/brahmkshatriya/echo/PlayerService.kt index b4cc84cc..15511550 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/PlayerService.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/PlayerService.kt @@ -2,9 +2,9 @@ package dev.brahmkshatriya.echo import android.annotation.SuppressLint import android.app.PendingIntent +import android.app.Service import android.content.Intent import android.content.SharedPreferences -import android.media.AudioManager import androidx.annotation.OptIn import androidx.media3.common.AudioAttributes import androidx.media3.common.C @@ -156,7 +156,7 @@ class PlayerService : MediaLibraryService() { exoPlayer.addListener( ControllerListener( exoPlayer, - getSystemService(AUDIO_SERVICE) as AudioManager, + this, scope, controllerList, throwFlow diff --git a/app/src/main/java/dev/brahmkshatriya/echo/playback/listeners/ControllerListener.kt b/app/src/main/java/dev/brahmkshatriya/echo/playback/listeners/ControllerListener.kt index e4d391a2..e75cd292 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/playback/listeners/ControllerListener.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/playback/listeners/ControllerListener.kt @@ -1,6 +1,10 @@ package dev.brahmkshatriya.echo.playback.listeners +import android.app.Service +import android.content.Context +import android.content.pm.ServiceInfo import android.media.AudioManager +import android.os.Build import androidx.media3.common.MediaItem import androidx.media3.common.Player import androidx.media3.common.Timeline @@ -20,11 +24,12 @@ import kotlinx.coroutines.withContext @UnstableApi class ControllerListener( player: Player, - private val audioManager: AudioManager, + private val service: Service, private val scope: CoroutineScope, private val controllerExtensions: MutableStateFlow?>, private val throwableFlow: MutableSharedFlow ) : PlayerListener(player) { + private var audioManager: AudioManager = service.getSystemService(Context.AUDIO_SERVICE) as AudioManager init { scope.launch { @@ -93,7 +98,19 @@ class ControllerListener( } } } + } + + @Suppress("DEPRECATION") // not being used in startForeground + private fun canRunCommand(): Boolean { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + return true + } + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + service.foregroundServiceType != ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE + } else { + true //if we are on Android 12 or lower, we can assume it's foreground or can be started as foreground + } } private suspend fun ControllerClient.tryOnMain( @@ -103,7 +120,7 @@ class ControllerListener( try { if (player.playbackState == Player.STATE_IDLE || player.playbackState == Player.STATE_ENDED || - player.isPlaying.not()) { + (!canRunCommand() && !player.isPlaying)) { return@withContext } block() @@ -124,6 +141,30 @@ class ControllerListener( } } + private fun updatePlaylist() { + val playlist = List(player.mediaItemCount) { index -> + player.getMediaItemAt(index).track + } + + notifyControllers { + onPlaylistChanged(playlist) + } + } + + private fun getVolume(): Double { + val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) + val volume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) + return volume.toDouble() / maxVolume.toDouble() + } + + + override fun onRenderedFirstFrame() { + super.onRenderedFirstFrame() + notifyControllers { + onVolumeChanged(getVolume()) + } + } + override fun onTrackStart(mediaItem: MediaItem) { val isPlaying = player.isPlaying val position = player.currentPosition.toDouble() @@ -144,16 +185,6 @@ class ControllerListener( updatePlaylist() } - private fun updatePlaylist() { - val playlist = List(player.mediaItemCount) { index -> - player.getMediaItemAt(index).track - } - - notifyControllers { - onPlaylistChanged(playlist) - } - } - override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { super.onMediaItemTransition(mediaItem, reason) } @@ -201,10 +232,8 @@ class ControllerListener( override fun onDeviceVolumeChanged(volume: Int, muted: Boolean) { super.onDeviceVolumeChanged(volume, muted) - val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) - val normalized = volume.toDouble() / maxVolume.toDouble() notifyControllers { - onVolumeChanged(normalized) + onVolumeChanged(getVolume()) } } } \ No newline at end of file From 0936a32ddfc0050b3b9c046be778965ee707fba5 Mon Sep 17 00:00:00 2001 From: rebel onion <87634197+rebelonion@users.noreply.github.com> Date: Wed, 20 Nov 2024 16:09:55 -0600 Subject: [PATCH 05/18] fix: move media controls to service when required --- app/src/main/AndroidManifest.xml | 7 +- .../dev/brahmkshatriya/echo/PlayerService.kt | 20 +-- .../extensions/ControllerExtensionService.kt | 118 +++++++++++++++++ .../extensions/ControllerServiceHelper.kt | 73 +++++++++++ .../playback/listeners/ControllerListener.kt | 122 ++++++++++++------ app/src/main/res/values/strings.xml | 3 + .../echo/common/clients/ControllerClient.kt | 74 +++++++++++ 7 files changed, 367 insertions(+), 50 deletions(-) create mode 100644 app/src/main/java/dev/brahmkshatriya/echo/extensions/ControllerExtensionService.kt create mode 100644 app/src/main/java/dev/brahmkshatriya/echo/extensions/ControllerServiceHelper.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 175cebd5..dbf4bd53 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -9,7 +9,7 @@ - + + + diff --git a/app/src/main/java/dev/brahmkshatriya/echo/PlayerService.kt b/app/src/main/java/dev/brahmkshatriya/echo/PlayerService.kt index 15511550..3e66bd1c 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/PlayerService.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/PlayerService.kt @@ -2,7 +2,6 @@ package dev.brahmkshatriya.echo import android.annotation.SuppressLint import android.app.PendingIntent -import android.app.Service import android.content.Intent import android.content.SharedPreferences import androidx.annotation.OptIn @@ -42,6 +41,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import javax.inject.Inject @AndroidEntryPoint +@UnstableApi class PlayerService : MediaLibraryService() { private var mediaSession: MediaLibrarySession? = null override fun onGetSession(controllerInfo: MediaSession.ControllerInfo) = mediaSession @@ -74,6 +74,8 @@ class PlayerService : MediaLibraryService() { @Inject lateinit var fftAudioProcessor: FFTAudioProcessor + lateinit var controllerListener: ControllerListener + private val scope = CoroutineScope(Dispatchers.Main) @OptIn(UnstableApi::class) @@ -153,15 +155,14 @@ class PlayerService : MediaLibraryService() { exoPlayer.addListener( TrackingListener(exoPlayer, scope, extListFlow, trackerList, throwFlow) ) - exoPlayer.addListener( - ControllerListener( - exoPlayer, - this, - scope, - controllerList, - throwFlow - ) + controllerListener = ControllerListener( + exoPlayer, + this, + scope, + controllerList, + throwFlow ) + exoPlayer.addListener(controllerListener) settings.registerOnSharedPreferenceChangeListener { prefs, key -> when (key) { SKIP_SILENCE -> exoPlayer.skipSilenceEnabled = prefs.getBoolean(key, true) @@ -181,6 +182,7 @@ class PlayerService : MediaLibraryService() { release() mediaSession = null } + if (::controllerListener.isInitialized) controllerListener.onDestroy() super.onDestroy() } diff --git a/app/src/main/java/dev/brahmkshatriya/echo/extensions/ControllerExtensionService.kt b/app/src/main/java/dev/brahmkshatriya/echo/extensions/ControllerExtensionService.kt new file mode 100644 index 00000000..2c81d778 --- /dev/null +++ b/app/src/main/java/dev/brahmkshatriya/echo/extensions/ControllerExtensionService.kt @@ -0,0 +1,118 @@ +package dev.brahmkshatriya.echo.extensions + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.Service +import android.content.Intent +import dev.brahmkshatriya.echo.R +import android.os.Binder +import android.os.Build +import android.os.IBinder +import androidx.core.app.NotificationCompat +import androidx.media3.common.util.UnstableApi +import androidx.media3.common.Player + +@UnstableApi +class ControllerExtensionService : Service() { + private val binder = LocalBinder() + private var player: Player? = null + + inner class LocalBinder : Binder() { + fun getService(): ControllerExtensionService = this@ControllerExtensionService + } + + override fun onCreate() { + super.onCreate() + createNotificationChannel() + } + + override fun onBind(intent: Intent): IBinder { + return binder + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + startForeground(NOTIFICATION_ID, createNotification()) + return START_STICKY + } + + private fun createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel( + NOTIFICATION_CHANNEL_ID, + getString(R.string.media_playback_controller), + NotificationManager.IMPORTANCE_LOW + ).apply { + description = getString(R.string.media_playback_controller_description) + setSound(null, null) + } + val notificationManager = getSystemService(NotificationManager::class.java) + notificationManager.createNotificationChannel(channel) + } + } + + private fun createNotification(): Notification { + return NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID) + .setContentTitle(getString(R.string.media_playback_controller)) + .setContentText(getString(R.string.media_playback_controller_running)) + .setSmallIcon(android.R.drawable.ic_media_play) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setOngoing(true) + .build() + } + + fun setPlayer(exoPlayer: Player) { + player = exoPlayer + } + + fun play() { + player?.play() + } + + fun pause() { + player?.pause() + } + + fun seekToNext() { + player?.seekToNextMediaItem() + } + + fun seekToPrevious() { + player?.seekToPreviousMediaItem() + } + + fun seekTo(position: Long) { + player?.seekTo(position) + } + + fun seekToMediaItem(index: Int) { + player?.seekTo(index, 0) + } + + fun moveMediaItem(fromIndex: Int, toIndex: Int) { + player?.moveMediaItem(fromIndex, toIndex) + } + + fun removeMediaItem(index: Int) { + player?.removeMediaItem(index) + } + + fun setShuffleMode(enabled: Boolean) { + player?.shuffleModeEnabled = enabled + } + + fun setRepeatMode(repeatMode: Int) { + player?.repeatMode = repeatMode + } + + override fun onDestroy() { + player = null + super.onDestroy() + } + + companion object { + private const val NOTIFICATION_ID = 1 + private const val NOTIFICATION_CHANNEL_ID = "media_playback_channel" + const val ACTION_START_SERVICE = "action.START_SERVICE" + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/brahmkshatriya/echo/extensions/ControllerServiceHelper.kt b/app/src/main/java/dev/brahmkshatriya/echo/extensions/ControllerServiceHelper.kt new file mode 100644 index 00000000..94f1724d --- /dev/null +++ b/app/src/main/java/dev/brahmkshatriya/echo/extensions/ControllerServiceHelper.kt @@ -0,0 +1,73 @@ +package dev.brahmkshatriya.echo.extensions + +import android.app.Service +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.os.Build +import android.os.IBinder +import androidx.media3.common.Player +import androidx.media3.common.util.UnstableApi + + +@UnstableApi +class ControllerServiceHelper(private val parentService: Service) { + private var mediaService: ControllerExtensionService? = null + private var isServiceBound = false + private var player: Player? = null + + private val serviceConnection = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + val binder = service as ControllerExtensionService.LocalBinder + mediaService = binder.getService() + isServiceBound = true + player?.let { mediaService?.setPlayer(it) } + } + + override fun onServiceDisconnected(name: ComponentName?) { + mediaService = null + isServiceBound = false + } + } + + fun startService(player: Player) { + this.player = player + + val intent = Intent(parentService, ControllerExtensionService::class.java).apply { + action = ControllerExtensionService.ACTION_START_SERVICE + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + parentService.startForegroundService(intent) + } else { + parentService.startService(intent) + } + + parentService.bindService( + Intent(parentService, ControllerExtensionService::class.java), + serviceConnection, + Context.BIND_AUTO_CREATE + ) + } + + fun stopService() { + if (isServiceBound) { + parentService.unbindService(serviceConnection) + isServiceBound = false + } + parentService.stopService(Intent(parentService, ControllerExtensionService::class.java)) + player = null + } + fun play() = mediaService?.play() + fun pause() = mediaService?.pause() + fun seekToNext() = mediaService?.seekToNext() + fun seekToPrevious() = mediaService?.seekToPrevious() + fun seekTo(position: Long) = mediaService?.seekTo(position) + fun seekToMediaItem(index: Int) = mediaService?.seekToMediaItem(index) + fun moveMediaItem(fromIndex: Int, toIndex: Int) = + mediaService?.moveMediaItem(fromIndex, toIndex) + fun removeMediaItem(index: Int) = mediaService?.removeMediaItem(index) + fun setShuffleMode(enabled: Boolean) = mediaService?.setShuffleMode(enabled) + fun setRepeatMode(repeatMode: Int) = mediaService?.setRepeatMode(repeatMode) +} \ No newline at end of file diff --git a/app/src/main/java/dev/brahmkshatriya/echo/playback/listeners/ControllerListener.kt b/app/src/main/java/dev/brahmkshatriya/echo/playback/listeners/ControllerListener.kt index e75cd292..c6493391 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/playback/listeners/ControllerListener.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/playback/listeners/ControllerListener.kt @@ -2,9 +2,7 @@ package dev.brahmkshatriya.echo.playback.listeners import android.app.Service import android.content.Context -import android.content.pm.ServiceInfo import android.media.AudioManager -import android.os.Build import androidx.media3.common.MediaItem import androidx.media3.common.Player import androidx.media3.common.Timeline @@ -12,6 +10,7 @@ import androidx.media3.common.Tracks import androidx.media3.common.util.UnstableApi import dev.brahmkshatriya.echo.common.ControllerExtension import dev.brahmkshatriya.echo.common.clients.ControllerClient +import dev.brahmkshatriya.echo.extensions.ControllerServiceHelper import dev.brahmkshatriya.echo.extensions.get import dev.brahmkshatriya.echo.playback.MediaItemUtils.track import kotlinx.coroutines.CoroutineScope @@ -20,16 +19,19 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.util.concurrent.atomic.AtomicBoolean @UnstableApi class ControllerListener( player: Player, - private val service: Service, + service: Service, private val scope: CoroutineScope, private val controllerExtensions: MutableStateFlow?>, private val throwableFlow: MutableSharedFlow ) : PlayerListener(player) { private var audioManager: AudioManager = service.getSystemService(Context.AUDIO_SERVICE) as AudioManager + private val serviceHelper = ControllerServiceHelper(service) + private val needsService: AtomicBoolean = AtomicBoolean(false) init { scope.launch { @@ -43,55 +45,110 @@ class ControllerListener( } } + fun onDestroy() { + serviceHelper.stopService() + } + private suspend fun registerController(extension: ControllerExtension) { extension.get(throwableFlow) { + if (runsDuringPause) { + if (!needsService.get()) { + serviceHelper.startService(player) + } + needsService.set(true) + } onPlayRequest = { - tryOnMain { - player.play() + tryOnMain(Player.COMMAND_PLAY_PAUSE) { + if (needsService.get()) { + serviceHelper.play() + } else { + player.play() + } } } onPauseRequest = { - tryOnMain { - player.pause() + tryOnMain(Player.COMMAND_PLAY_PAUSE) { + if (needsService.get()) { + serviceHelper.pause() + } else { + player.pause() + } } } onNextRequest = { - tryOnMain { - player.seekToNextMediaItem() + tryOnMain(Player.COMMAND_SEEK_TO_NEXT) { + if (needsService.get()) { + serviceHelper.seekToNext() + } else { + player.seekToNextMediaItem() + } } } onPreviousRequest = { - tryOnMain { - player.seekToPreviousMediaItem() + tryOnMain(Player.COMMAND_SEEK_TO_PREVIOUS) { + if (needsService.get()) { + serviceHelper.seekToPrevious() + } else { + player.seekToPreviousMediaItem() + } } } onSeekRequest = { position -> - tryOnMain { - player.seekTo(position.toLong()) + tryOnMain(Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM) { + if (needsService.get()) { + serviceHelper.seekTo(position.toLong()) + } else { + player.seekTo(position.toLong()) + } + } + } + onSeekToMediaItemRequest = { index -> + tryOnMain(Player.COMMAND_SEEK_TO_MEDIA_ITEM) { + if (needsService.get()) { + serviceHelper.seekToMediaItem(index) + } else { + player.seekTo(index, 0) + } } } onMovePlaylistItemRequest = { fromIndex, toIndex -> - tryOnMain { - player.moveMediaItem(fromIndex, toIndex) + tryOnMain(Player.COMMAND_CHANGE_MEDIA_ITEMS) { + if (needsService.get()) { + serviceHelper.moveMediaItem(fromIndex, toIndex) + } else { + player.moveMediaItem(fromIndex, toIndex) + } } } onRemovePlaylistItemRequest = { index -> - tryOnMain { - player.removeMediaItem(index) + tryOnMain(Player.COMMAND_CHANGE_MEDIA_ITEMS) { + if (needsService.get()) { + serviceHelper.removeMediaItem(index) + } else { + player.removeMediaItem(index) + } } } onShuffleModeRequest = { enabled -> - tryOnMain { - player.shuffleModeEnabled = enabled + tryOnMain(Player.COMMAND_SET_SHUFFLE_MODE) { + if (needsService.get()) { + serviceHelper.setShuffleMode(enabled) + } else { + player.shuffleModeEnabled = enabled + } } } onRepeatModeRequest = { repeatMode -> - tryOnMain { - player.repeatMode = repeatMode + tryOnMain(Player.COMMAND_SET_REPEAT_MODE) { + if (needsService.get()) { + serviceHelper.setRepeatMode(repeatMode) + } else { + player.repeatMode = repeatMode + } } } onVolumeRequest = { volume -> - tryOnMain { + tryOnMain(-1) { // not an exoplayer command val denormalized = volume * audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, denormalized.toInt(), 0) @@ -100,30 +157,15 @@ class ControllerListener( } } - @Suppress("DEPRECATION") // not being used in startForeground - private fun canRunCommand(): Boolean { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { - return true - } - - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - service.foregroundServiceType != ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE - } else { - true //if we are on Android 12 or lower, we can assume it's foreground or can be started as foreground - } - } - private suspend fun ControllerClient.tryOnMain( + command: Int, block: suspend ControllerClient.() -> Unit ) { withContext(Dispatchers.Main.immediate) { try { - if (player.playbackState == Player.STATE_IDLE || - player.playbackState == Player.STATE_ENDED || - (!canRunCommand() && !player.isPlaying)) { - return@withContext + if (command == -1 || player.isCommandAvailable(command) == true) { + block() } - block() } catch (e: Exception) { throwableFlow.emit(e) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 287ce9a8..386ebb2c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -268,6 +268,9 @@ Backgrounds Source Selection Controller + Media Playback Controller + Control media playback from controller extensions + Media playback controller is running Highest Medium diff --git a/common/src/main/java/dev/brahmkshatriya/echo/common/clients/ControllerClient.kt b/common/src/main/java/dev/brahmkshatriya/echo/common/clients/ControllerClient.kt index 342a8fb3..a16e2f98 100644 --- a/common/src/main/java/dev/brahmkshatriya/echo/common/clients/ControllerClient.kt +++ b/common/src/main/java/dev/brahmkshatriya/echo/common/clients/ControllerClient.kt @@ -3,22 +3,96 @@ package dev.brahmkshatriya.echo.common.clients import dev.brahmkshatriya.echo.common.models.Track interface ControllerClient : ExtensionClient { + /** + * Whether the the extension can perform actions when the player is paused. + * Only set this to false if you are sure that the extension does not need to perform any actions when the player is paused. + */ + var runsDuringPause: Boolean + // app -> controller + /** + * Called when the playback state changes. + * @param isPlaying Whether the player is playing. + * @param position The current position of the track. + * @param track The current track in the player. + */ suspend fun onPlaybackStateChanged(isPlaying: Boolean, position: Double, track: Track) + /** + * Called when the playlist changes. + * @param playlist The new playlist. + * The first track is not necessarily the current track. + */ suspend fun onPlaylistChanged(playlist: List) + /** + * Called when the playback mode changes. + * @param isShuffle Whether shuffle mode is enabled. + * @param repeatState The repeat mode. + * @see androidx.media3.common.Player.REPEAT_MODE_OFF + */ suspend fun onPlaybackModeChanged(isShuffle: Boolean, repeatState: Int) + /** + * Called when the position of the track changes. + * @param position The new position of the track. + */ suspend fun onPositionChanged(position: Double) + /** + * Called when the volume of the player changes. + * @param volume The new volume of the player. + */ suspend fun onVolumeChanged(volume: Double) // controller -> app + /** + * Called when the controller requests to play the player. + */ var onPlayRequest: (suspend () -> Unit)? + /** + * Called when the controller requests to pause the player. + */ var onPauseRequest: (suspend () -> Unit)? + /** + * Called when the controller requests to play the next track. + */ var onNextRequest: (suspend () -> Unit)? + /** + * Called when the controller requests to play the previous track. + */ var onPreviousRequest: (suspend () -> Unit)? + /** + * Called when the controller requests to seek to a position in the track. + * @param position The position to seek to. + */ var onSeekRequest: (suspend (position: Double) -> Unit)? + /** + * Called when the controller requests to seek to a track in the playlist. + * @param index The index of the track to seek to. + */ + var onSeekToMediaItemRequest: (suspend (index: Int) -> Unit)? + /** + * Called when the controller requests to move a track in the playlist. + * @param fromIndex The index of the track to move. + * @param toIndex The index to move the track to. + */ var onMovePlaylistItemRequest: (suspend (fromIndex: Int, toIndex: Int) -> Unit)? + /** + * Called when the controller requests to remove a track from the playlist. + * @param index The index of the track to remove. + */ var onRemovePlaylistItemRequest: (suspend (index: Int) -> Unit)? + /** + * Called when the controller requests to enable or disable shuffle mode. + * @param enabled Whether shuffle mode should be enabled. + */ var onShuffleModeRequest: (suspend (enabled: Boolean) -> Unit)? + /** + * Called when the controller requests to change the repeat mode. + * @param repeatMode The new repeat mode. + * @see androidx.media3.common.Player.REPEAT_MODE_OFF + */ var onRepeatModeRequest: (suspend (repeatMode: Int) -> Unit)? + /** + * Called when the controller requests to change the volume of the player. + * @param volume The new volume of the player. + */ var onVolumeRequest: (suspend (volume: Double) -> Unit)? } \ No newline at end of file From 3c15c4c1b39f0afbbfe0201aa95db66b3f6245ad Mon Sep 17 00:00:00 2001 From: rebel onion <87634197+rebelonion@users.noreply.github.com> Date: Wed, 20 Nov 2024 19:35:25 -0600 Subject: [PATCH 06/18] feat: more controller options --- .../extensions/ControllerExtensionService.kt | 5 +- .../extensions/ControllerServiceHelper.kt | 3 +- .../playback/listeners/ControllerListener.kt | 60 ++++++++++---- .../echo/common/clients/ControllerClient.kt | 83 +++++++++++++++---- 4 files changed, 116 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/dev/brahmkshatriya/echo/extensions/ControllerExtensionService.kt b/app/src/main/java/dev/brahmkshatriya/echo/extensions/ControllerExtensionService.kt index 2c81d778..b5f1d3bc 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/extensions/ControllerExtensionService.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/extensions/ControllerExtensionService.kt @@ -12,6 +12,7 @@ import android.os.IBinder import androidx.core.app.NotificationCompat import androidx.media3.common.util.UnstableApi import androidx.media3.common.Player +import dev.brahmkshatriya.echo.common.clients.ControllerClient.RepeatMode @UnstableApi class ControllerExtensionService : Service() { @@ -101,8 +102,8 @@ class ControllerExtensionService : Service() { player?.shuffleModeEnabled = enabled } - fun setRepeatMode(repeatMode: Int) { - player?.repeatMode = repeatMode + fun setRepeatMode(repeatMode: RepeatMode) { + player?.repeatMode = repeatMode.ordinal } override fun onDestroy() { diff --git a/app/src/main/java/dev/brahmkshatriya/echo/extensions/ControllerServiceHelper.kt b/app/src/main/java/dev/brahmkshatriya/echo/extensions/ControllerServiceHelper.kt index 94f1724d..715cee3d 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/extensions/ControllerServiceHelper.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/extensions/ControllerServiceHelper.kt @@ -9,6 +9,7 @@ import android.os.Build import android.os.IBinder import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi +import dev.brahmkshatriya.echo.common.clients.ControllerClient.RepeatMode @UnstableApi @@ -69,5 +70,5 @@ class ControllerServiceHelper(private val parentService: Service) { mediaService?.moveMediaItem(fromIndex, toIndex) fun removeMediaItem(index: Int) = mediaService?.removeMediaItem(index) fun setShuffleMode(enabled: Boolean) = mediaService?.setShuffleMode(enabled) - fun setRepeatMode(repeatMode: Int) = mediaService?.setRepeatMode(repeatMode) + fun setRepeatMode(repeatMode: RepeatMode) = mediaService?.setRepeatMode(repeatMode) } \ No newline at end of file diff --git a/app/src/main/java/dev/brahmkshatriya/echo/playback/listeners/ControllerListener.kt b/app/src/main/java/dev/brahmkshatriya/echo/playback/listeners/ControllerListener.kt index c6493391..b82e6aea 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/playback/listeners/ControllerListener.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/playback/listeners/ControllerListener.kt @@ -10,6 +10,9 @@ import androidx.media3.common.Tracks import androidx.media3.common.util.UnstableApi import dev.brahmkshatriya.echo.common.ControllerExtension import dev.brahmkshatriya.echo.common.clients.ControllerClient +import dev.brahmkshatriya.echo.common.clients.ControllerClient.PlayerState +import dev.brahmkshatriya.echo.common.clients.ControllerClient.RepeatMode +import dev.brahmkshatriya.echo.common.models.Track import dev.brahmkshatriya.echo.extensions.ControllerServiceHelper import dev.brahmkshatriya.echo.extensions.get import dev.brahmkshatriya.echo.playback.MediaItemUtils.track @@ -29,7 +32,8 @@ class ControllerListener( private val controllerExtensions: MutableStateFlow?>, private val throwableFlow: MutableSharedFlow ) : PlayerListener(player) { - private var audioManager: AudioManager = service.getSystemService(Context.AUDIO_SERVICE) as AudioManager + private var audioManager: AudioManager = + service.getSystemService(Context.AUDIO_SERVICE) as AudioManager private val serviceHelper = ControllerServiceHelper(service) private val needsService: AtomicBoolean = AtomicBoolean(false) @@ -46,6 +50,9 @@ class ControllerListener( } fun onDestroy() { + notifyControllers { + onPlaybackStateChanged(false, 0, null) + } serviceHelper.stopService() } @@ -143,7 +150,7 @@ class ControllerListener( if (needsService.get()) { serviceHelper.setRepeatMode(repeatMode) } else { - player.repeatMode = repeatMode + player.repeatMode = repeatMode.ordinal } } } @@ -154,20 +161,39 @@ class ControllerListener( audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, denormalized.toInt(), 0) } } + onRequestState = { + tryOnMain(-1) { + val playlist = getPlaylist() + val currentTrack = player.currentMediaItem?.track + PlayerState( + player.isPlaying, + currentTrack, + player.currentPosition, + playlist, + playlist.indexOf(currentTrack), + player.shuffleModeEnabled, + RepeatMode.entries[player.repeatMode], + getVolume() + ) + } ?: PlayerState() + } } } - private suspend fun ControllerClient.tryOnMain( + private suspend fun ControllerClient.tryOnMain( command: Int, - block: suspend ControllerClient.() -> Unit - ) { - withContext(Dispatchers.Main.immediate) { + block: suspend ControllerClient.() -> T? + ): T? { + return withContext(Dispatchers.Main.immediate) { try { if (command == -1 || player.isCommandAvailable(command) == true) { block() + } else { + null } } catch (e: Exception) { throwableFlow.emit(e) + null } } } @@ -183,20 +209,24 @@ class ControllerListener( } } - private fun updatePlaylist() { + private fun getPlaylist(): List { val playlist = List(player.mediaItemCount) { index -> player.getMediaItemAt(index).track } + return playlist + } + + private fun updatePlaylist() { notifyControllers { - onPlaylistChanged(playlist) + onPlaylistChanged(getPlaylist()) } } private fun getVolume(): Double { val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) val volume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) - return volume.toDouble() / maxVolume.toDouble() + return (volume.toDouble() / maxVolume.toDouble()) } @@ -209,7 +239,7 @@ class ControllerListener( override fun onTrackStart(mediaItem: MediaItem) { val isPlaying = player.isPlaying - val position = player.currentPosition.toDouble() + val position = player.currentPosition val track = mediaItem.track notifyControllers { @@ -233,7 +263,7 @@ class ControllerListener( override fun onIsPlayingChanged(isPlaying: Boolean) { super.onIsPlayingChanged(isPlaying) - val position = player.currentPosition.toDouble() + val position = player.currentPosition val track = player.currentMediaItem?.track ?: return notifyControllers { @@ -246,7 +276,7 @@ class ControllerListener( val repeatMode = player.repeatMode notifyControllers { - onPlaybackModeChanged(shuffleModeEnabled, repeatMode) + onPlaybackModeChanged(shuffleModeEnabled, RepeatMode.entries[repeatMode]) } } @@ -255,7 +285,7 @@ class ControllerListener( val shuffleModeEnabled = player.shuffleModeEnabled notifyControllers { - onPlaybackModeChanged(shuffleModeEnabled, repeatMode) + onPlaybackModeChanged(shuffleModeEnabled, RepeatMode.entries[repeatMode]) } } @@ -265,10 +295,8 @@ class ControllerListener( reason: Int ) { super.onPositionDiscontinuity(oldPosition, newPosition, reason) - val position = newPosition.positionMs.toDouble() - notifyControllers { - onPositionChanged(position) + onPositionChanged(newPosition.positionMs) } } diff --git a/common/src/main/java/dev/brahmkshatriya/echo/common/clients/ControllerClient.kt b/common/src/main/java/dev/brahmkshatriya/echo/common/clients/ControllerClient.kt index a16e2f98..ceff2538 100644 --- a/common/src/main/java/dev/brahmkshatriya/echo/common/clients/ControllerClient.kt +++ b/common/src/main/java/dev/brahmkshatriya/echo/common/clients/ControllerClient.kt @@ -1,8 +1,40 @@ package dev.brahmkshatriya.echo.common.clients import dev.brahmkshatriya.echo.common.models.Track +import kotlinx.serialization.Serializable interface ControllerClient : ExtensionClient { + @Serializable + enum class RepeatMode { + OFF, + ONE, + ALL + } + + /** + * The current state of the player. + * @param isPlaying Whether the player is playing. + * @param currentTrack The current track in the player. + * @param currentPosition The current position of the track. + * @param playlist The current playlist. + * @param currentIndex The index of the current track in the playlist. -1 if the playlist is empty. + * @param shuffle Whether shuffle mode is enabled. + * @param repeatMode The repeat mode. + * @see RepeatMode + * @param volume The volume of the player normalized between 0 and 1. + */ + @Serializable + data class PlayerState( + val isPlaying: Boolean = false, + val currentTrack: Track? = null, + val currentPosition: Long = 0, + val playlist: List = emptyList(), + val currentIndex: Int = 0, + val shuffle: Boolean = false, + val repeatMode: RepeatMode = RepeatMode.OFF, + val volume: Double = 0.0 + ) + /** * Whether the the extension can perform actions when the player is paused. * Only set this to false if you are sure that the extension does not need to perform any actions when the player is paused. @@ -16,25 +48,29 @@ interface ControllerClient : ExtensionClient { * @param position The current position of the track. * @param track The current track in the player. */ - suspend fun onPlaybackStateChanged(isPlaying: Boolean, position: Double, track: Track) + suspend fun onPlaybackStateChanged(isPlaying: Boolean, position: Long, track: Track?) + /** * Called when the playlist changes. * @param playlist The new playlist. * The first track is not necessarily the current track. */ suspend fun onPlaylistChanged(playlist: List) + /** * Called when the playback mode changes. * @param isShuffle Whether shuffle mode is enabled. - * @param repeatState The repeat mode. - * @see androidx.media3.common.Player.REPEAT_MODE_OFF + * @param repeatMode The repeat mode. + * @see RepeatMode */ - suspend fun onPlaybackModeChanged(isShuffle: Boolean, repeatState: Int) + suspend fun onPlaybackModeChanged(isShuffle: Boolean, repeatMode: RepeatMode) + /** * Called when the position of the track changes. * @param position The new position of the track. */ - suspend fun onPositionChanged(position: Double) + suspend fun onPositionChanged(position: Long) + /** * Called when the volume of the player changes. * @param volume The new volume of the player. @@ -42,57 +78,72 @@ interface ControllerClient : ExtensionClient { suspend fun onVolumeChanged(volume: Double) // controller -> app + /** + * Called when the controller requests the current state of the player. + * @return The current state of the player. + */ + var onRequestState: (suspend () -> PlayerState)? + /** * Called when the controller requests to play the player. */ var onPlayRequest: (suspend () -> Unit)? + /** * Called when the controller requests to pause the player. */ var onPauseRequest: (suspend () -> Unit)? + /** * Called when the controller requests to play the next track. */ var onNextRequest: (suspend () -> Unit)? + /** * Called when the controller requests to play the previous track. */ var onPreviousRequest: (suspend () -> Unit)? + /** * Called when the controller requests to seek to a position in the track. - * @param position The position to seek to. + * passes the position in milliseconds. */ - var onSeekRequest: (suspend (position: Double) -> Unit)? + var onSeekRequest: (suspend (position: Long) -> Unit)? + /** * Called when the controller requests to seek to a track in the playlist. - * @param index The index of the track to seek to. + * passes the index of the track. */ var onSeekToMediaItemRequest: (suspend (index: Int) -> Unit)? + /** * Called when the controller requests to move a track in the playlist. - * @param fromIndex The index of the track to move. - * @param toIndex The index to move the track to. + * passes the index of the track to move and the new index. */ var onMovePlaylistItemRequest: (suspend (fromIndex: Int, toIndex: Int) -> Unit)? + /** * Called when the controller requests to remove a track from the playlist. - * @param index The index of the track to remove. + * passes the index of the track to remove. */ var onRemovePlaylistItemRequest: (suspend (index: Int) -> Unit)? + /** * Called when the controller requests to enable or disable shuffle mode. - * @param enabled Whether shuffle mode should be enabled. + * passes whether shuffle mode should be enabled. */ var onShuffleModeRequest: (suspend (enabled: Boolean) -> Unit)? + /** * Called when the controller requests to change the repeat mode. - * @param repeatMode The new repeat mode. - * @see androidx.media3.common.Player.REPEAT_MODE_OFF + * passes the new repeat mode. + * @see RepeatMode */ - var onRepeatModeRequest: (suspend (repeatMode: Int) -> Unit)? + var onRepeatModeRequest: (suspend (repeatMode: RepeatMode) -> Unit)? + /** * Called when the controller requests to change the volume of the player. - * @param volume The new volume of the player. + * passes the new volume of the player. */ var onVolumeRequest: (suspend (volume: Double) -> Unit)? } \ No newline at end of file From 2379d6f75f3ef1b4155c0effb81e9cba212a246e Mon Sep 17 00:00:00 2001 From: rebel onion <87634197+rebelonion@users.noreply.github.com> Date: Wed, 20 Nov 2024 20:27:46 -0600 Subject: [PATCH 07/18] fix: make ControllerClient an abstract class --- .../echo/common/clients/ControllerClient.kt | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/common/src/main/java/dev/brahmkshatriya/echo/common/clients/ControllerClient.kt b/common/src/main/java/dev/brahmkshatriya/echo/common/clients/ControllerClient.kt index ceff2538..8c20b9f9 100644 --- a/common/src/main/java/dev/brahmkshatriya/echo/common/clients/ControllerClient.kt +++ b/common/src/main/java/dev/brahmkshatriya/echo/common/clients/ControllerClient.kt @@ -3,7 +3,7 @@ package dev.brahmkshatriya.echo.common.clients import dev.brahmkshatriya.echo.common.models.Track import kotlinx.serialization.Serializable -interface ControllerClient : ExtensionClient { +abstract class ControllerClient : ExtensionClient { @Serializable enum class RepeatMode { OFF, @@ -39,7 +39,7 @@ interface ControllerClient : ExtensionClient { * Whether the the extension can perform actions when the player is paused. * Only set this to false if you are sure that the extension does not need to perform any actions when the player is paused. */ - var runsDuringPause: Boolean + abstract var runsDuringPause: Boolean // app -> controller /** @@ -48,14 +48,14 @@ interface ControllerClient : ExtensionClient { * @param position The current position of the track. * @param track The current track in the player. */ - suspend fun onPlaybackStateChanged(isPlaying: Boolean, position: Long, track: Track?) + abstract suspend fun onPlaybackStateChanged(isPlaying: Boolean, position: Long, track: Track?) /** * Called when the playlist changes. * @param playlist The new playlist. * The first track is not necessarily the current track. */ - suspend fun onPlaylistChanged(playlist: List) + abstract suspend fun onPlaylistChanged(playlist: List) /** * Called when the playback mode changes. @@ -63,87 +63,87 @@ interface ControllerClient : ExtensionClient { * @param repeatMode The repeat mode. * @see RepeatMode */ - suspend fun onPlaybackModeChanged(isShuffle: Boolean, repeatMode: RepeatMode) + abstract suspend fun onPlaybackModeChanged(isShuffle: Boolean, repeatMode: RepeatMode) /** * Called when the position of the track changes. * @param position The new position of the track. */ - suspend fun onPositionChanged(position: Long) + abstract suspend fun onPositionChanged(position: Long) /** * Called when the volume of the player changes. * @param volume The new volume of the player. */ - suspend fun onVolumeChanged(volume: Double) + abstract suspend fun onVolumeChanged(volume: Double) // controller -> app /** * Called when the controller requests the current state of the player. * @return The current state of the player. */ - var onRequestState: (suspend () -> PlayerState)? + var onRequestState: (suspend () -> PlayerState)? = null /** * Called when the controller requests to play the player. */ - var onPlayRequest: (suspend () -> Unit)? + var onPlayRequest: (suspend () -> Unit)? = null /** * Called when the controller requests to pause the player. */ - var onPauseRequest: (suspend () -> Unit)? + var onPauseRequest: (suspend () -> Unit)? = null /** * Called when the controller requests to play the next track. */ - var onNextRequest: (suspend () -> Unit)? + var onNextRequest: (suspend () -> Unit)? = null /** * Called when the controller requests to play the previous track. */ - var onPreviousRequest: (suspend () -> Unit)? + var onPreviousRequest: (suspend () -> Unit)? = null /** * Called when the controller requests to seek to a position in the track. * passes the position in milliseconds. */ - var onSeekRequest: (suspend (position: Long) -> Unit)? + var onSeekRequest: (suspend (position: Long) -> Unit)? = null /** * Called when the controller requests to seek to a track in the playlist. * passes the index of the track. */ - var onSeekToMediaItemRequest: (suspend (index: Int) -> Unit)? + var onSeekToMediaItemRequest: (suspend (index: Int) -> Unit)? = null /** * Called when the controller requests to move a track in the playlist. * passes the index of the track to move and the new index. */ - var onMovePlaylistItemRequest: (suspend (fromIndex: Int, toIndex: Int) -> Unit)? + var onMovePlaylistItemRequest: (suspend (fromIndex: Int, toIndex: Int) -> Unit)? = null /** * Called when the controller requests to remove a track from the playlist. * passes the index of the track to remove. */ - var onRemovePlaylistItemRequest: (suspend (index: Int) -> Unit)? + var onRemovePlaylistItemRequest: (suspend (index: Int) -> Unit)? = null /** * Called when the controller requests to enable or disable shuffle mode. * passes whether shuffle mode should be enabled. */ - var onShuffleModeRequest: (suspend (enabled: Boolean) -> Unit)? + var onShuffleModeRequest: (suspend (enabled: Boolean) -> Unit)? = null /** * Called when the controller requests to change the repeat mode. * passes the new repeat mode. * @see RepeatMode */ - var onRepeatModeRequest: (suspend (repeatMode: RepeatMode) -> Unit)? + var onRepeatModeRequest: (suspend (repeatMode: RepeatMode) -> Unit)? = null /** * Called when the controller requests to change the volume of the player. * passes the new volume of the player. */ - var onVolumeRequest: (suspend (volume: Double) -> Unit)? + var onVolumeRequest: (suspend (volume: Double) -> Unit)? = null } \ No newline at end of file From c66f74e27d43d707bae243e05e42c476e5853b38 Mon Sep 17 00:00:00 2001 From: rebel onion <87634197+rebelonion@users.noreply.github.com> Date: Wed, 20 Nov 2024 22:04:13 -0600 Subject: [PATCH 08/18] chore: code clean --- .../echo/playback/listeners/ControllerListener.kt | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/dev/brahmkshatriya/echo/playback/listeners/ControllerListener.kt b/app/src/main/java/dev/brahmkshatriya/echo/playback/listeners/ControllerListener.kt index b82e6aea..faa85be5 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/playback/listeners/ControllerListener.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/playback/listeners/ControllerListener.kt @@ -103,9 +103,9 @@ class ControllerListener( onSeekRequest = { position -> tryOnMain(Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM) { if (needsService.get()) { - serviceHelper.seekTo(position.toLong()) + serviceHelper.seekTo(position) } else { - player.seekTo(position.toLong()) + player.seekTo(position) } } } @@ -186,7 +186,7 @@ class ControllerListener( ): T? { return withContext(Dispatchers.Main.immediate) { try { - if (command == -1 || player.isCommandAvailable(command) == true) { + if (command == -1 || player.isCommandAvailable(command)) { block() } else { null @@ -257,10 +257,6 @@ class ControllerListener( updatePlaylist() } - override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { - super.onMediaItemTransition(mediaItem, reason) - } - override fun onIsPlayingChanged(isPlaying: Boolean) { super.onIsPlayingChanged(isPlaying) val position = player.currentPosition From 5ddb524eae53a9c37e7c9dfe62bab1fa33117d7c Mon Sep 17 00:00:00 2001 From: rebel onion <87634197+rebelonion@users.noreply.github.com> Date: Wed, 20 Nov 2024 22:38:25 -0600 Subject: [PATCH 09/18] feat: message client --- .../brahmkshatriya/echo/di/ExtensionModule.kt | 3 +++ .../echo/extensions/ExtensionLoader.kt | 16 +++++++++++++++- .../echo/common/clients/MessagePostClient.kt | 5 +++++ 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 common/src/main/java/dev/brahmkshatriya/echo/common/clients/MessagePostClient.kt diff --git a/app/src/main/java/dev/brahmkshatriya/echo/di/ExtensionModule.kt b/app/src/main/java/dev/brahmkshatriya/echo/di/ExtensionModule.kt index fd6b4187..a29346f7 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/di/ExtensionModule.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/di/ExtensionModule.kt @@ -17,6 +17,7 @@ import dev.brahmkshatriya.echo.common.TrackerExtension import dev.brahmkshatriya.echo.db.models.UserEntity import dev.brahmkshatriya.echo.extensions.ExtensionLoader import dev.brahmkshatriya.echo.offline.OfflineExtension +import dev.brahmkshatriya.echo.viewmodels.SnackBar import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import javax.inject.Singleton @@ -60,6 +61,7 @@ class ExtensionModule { fun provideExtensionLoader( context: Application, throwableFlow: MutableSharedFlow, + mutableMessageFlow: MutableSharedFlow, database: EchoDatabase, settings: SharedPreferences, refresher: MutableSharedFlow, @@ -77,6 +79,7 @@ class ExtensionModule { context, offlineExtension, throwableFlow, + mutableMessageFlow, extensionDao, userDao, settings, diff --git a/app/src/main/java/dev/brahmkshatriya/echo/extensions/ExtensionLoader.kt b/app/src/main/java/dev/brahmkshatriya/echo/extensions/ExtensionLoader.kt index 5ee4980a..b6848dcd 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/extensions/ExtensionLoader.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/extensions/ExtensionLoader.kt @@ -9,6 +9,7 @@ import dev.brahmkshatriya.echo.common.MusicExtension import dev.brahmkshatriya.echo.common.TrackerExtension import dev.brahmkshatriya.echo.common.clients.ExtensionClient import dev.brahmkshatriya.echo.common.clients.LoginClient +import dev.brahmkshatriya.echo.common.clients.MessagePostClient import dev.brahmkshatriya.echo.common.helpers.ExtensionType import dev.brahmkshatriya.echo.common.models.Metadata import dev.brahmkshatriya.echo.common.providers.ControllerClientsProvider @@ -24,6 +25,7 @@ import dev.brahmkshatriya.echo.extensions.plugger.PackageChangeListener import dev.brahmkshatriya.echo.offline.BuiltInExtensionRepo import dev.brahmkshatriya.echo.offline.OfflineExtension import dev.brahmkshatriya.echo.utils.catchWith +import dev.brahmkshatriya.echo.viewmodels.SnackBar import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -47,6 +49,7 @@ class ExtensionLoader( context: Context, offlineExtension: OfflineExtension, private val throwableFlow: MutableSharedFlow, + private val mutableMessageFlow: MutableSharedFlow, private val extensionDao: ExtensionDao, private val userDao: UserDao, private val settings: SharedPreferences, @@ -114,7 +117,7 @@ class ExtensionLoader( //Inject other extensions launch { val combined = merge( - extensionFlow.map { listOfNotNull(it) }, trackerListFlow, lyricsListFlow + extensionFlow.map { listOfNotNull(it) }, trackerListFlow, lyricsListFlow, controllerListFlow ) combined.collect { list -> val trackerExtensions = trackerListFlow.value.orEmpty() @@ -142,6 +145,9 @@ class ExtensionLoader( setMusicExtensions(it) } } + extension.get(throwableFlow) { + registerMessagePostClient(this) + } } } } @@ -262,6 +268,14 @@ class ExtensionLoader( ?.let { metadata.copy(enabled = it) } ?: metadata } + private fun registerMessagePostClient(client: MessagePostClient) { + client.postMessage = { message -> + scope.launch { + mutableMessageFlow.emit(SnackBar.Message(message)) + } + } + } + companion object { const val LAST_EXTENSION_KEY = "last_extension" private const val TIMEOUT = 5000L diff --git a/common/src/main/java/dev/brahmkshatriya/echo/common/clients/MessagePostClient.kt b/common/src/main/java/dev/brahmkshatriya/echo/common/clients/MessagePostClient.kt new file mode 100644 index 00000000..03375679 --- /dev/null +++ b/common/src/main/java/dev/brahmkshatriya/echo/common/clients/MessagePostClient.kt @@ -0,0 +1,5 @@ +package dev.brahmkshatriya.echo.common.clients + +abstract class MessagePostClient { + var postMessage: (String) -> Unit = {} +} \ No newline at end of file From b78d78839796844ddb23bb66d94dac32f3311314 Mon Sep 17 00:00:00 2001 From: rebel onion <87634197+rebelonion@users.noreply.github.com> Date: Wed, 20 Nov 2024 23:38:05 -0600 Subject: [PATCH 10/18] fix: move where message client is registered --- .../echo/extensions/ExtensionLoader.kt | 34 +++++++++++-------- .../echo/viewmodels/ExtensionViewModel.kt | 2 +- .../echo/common/clients/MessagePostClient.kt | 4 +-- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/dev/brahmkshatriya/echo/extensions/ExtensionLoader.kt b/app/src/main/java/dev/brahmkshatriya/echo/extensions/ExtensionLoader.kt index b6848dcd..dec73ad8 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/extensions/ExtensionLoader.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/extensions/ExtensionLoader.kt @@ -145,9 +145,6 @@ class ExtensionLoader( setMusicExtensions(it) } } - extension.get(throwableFlow) { - registerMessagePostClient(this) - } } } } @@ -225,7 +222,7 @@ class ExtensionLoader( val id = settings.getString(LAST_EXTENSION_KEY, null) val extension = extensions.find { it.metadata.id == id } ?: extensions.firstOrNull() setupMusicExtension( - scope, settings, extensionFlow, userDao, userFlow, throwableFlow, extension + scope, settings, extensionFlow, userDao, userFlow, throwableFlow, mutableMessageFlow, extension ) refresher.emit(false) music.emit(Unit) @@ -257,7 +254,7 @@ class ExtensionLoader( private suspend fun List>.setExtensions() = coroutineScope { map { async { - setExtension(userDao, userFlow, throwableFlow, it) + setExtension(userDao, userFlow, throwableFlow, mutableMessageFlow, it) } }.awaitAll() } @@ -268,14 +265,6 @@ class ExtensionLoader( ?.let { metadata.copy(enabled = it) } ?: metadata } - private fun registerMessagePostClient(client: MessagePostClient) { - client.postMessage = { message -> - scope.launch { - mutableMessageFlow.emit(SnackBar.Message(message)) - } - } - } - companion object { const val LAST_EXTENSION_KEY = "last_extension" private const val TIMEOUT = 5000L @@ -289,12 +278,13 @@ class ExtensionLoader( userDao: UserDao, userFlow: MutableSharedFlow, throwableFlow: MutableSharedFlow, + mutableMessageFlow: MutableSharedFlow, extension: MusicExtension? ) { settings.edit().putString(LAST_EXTENSION_KEY, extension?.id).apply() extension?.takeIf { it.metadata.enabled } ?: return scope.launch { - setExtension(userDao, userFlow, throwableFlow, extension) + setExtension(userDao, userFlow, throwableFlow, mutableMessageFlow, extension) extensionFlow.value = extension } } @@ -303,8 +293,12 @@ class ExtensionLoader( userDao: UserDao, userFlow: MutableSharedFlow, throwableFlow: MutableSharedFlow, + mutableMessageFlow: MutableSharedFlow, extension: Extension<*>, ) = withContext(Dispatchers.IO) { + extension.get(throwableFlow){ + registerMessagePostClient(this, this@withContext, mutableMessageFlow) + } extension.run(throwableFlow) { withTimeout(TIMEOUT) { onExtensionSelected() } } @@ -323,5 +317,17 @@ class ExtensionLoader( } if (success != null) flow.emit(user) } + + private fun registerMessagePostClient( + client: MessagePostClient, + scope: CoroutineScope, + mutableMessageFlow: MutableSharedFlow + ) { + client.postMessage = { message -> + scope.launch(Dispatchers.Main) { + mutableMessageFlow.emit(SnackBar.Message(message)) + } + } + } } } \ No newline at end of file diff --git a/app/src/main/java/dev/brahmkshatriya/echo/viewmodels/ExtensionViewModel.kt b/app/src/main/java/dev/brahmkshatriya/echo/viewmodels/ExtensionViewModel.kt index d6ae1c6c..cc4e6766 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/viewmodels/ExtensionViewModel.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/viewmodels/ExtensionViewModel.kt @@ -86,7 +86,7 @@ class ExtensionViewModel @Inject constructor( private val userDao = database.userDao() fun setExtension(extension: MusicExtension?) { setupMusicExtension( - viewModelScope, settings, extensionFlow, userDao, userFlow, throwableFlow, extension + viewModelScope, settings, extensionFlow, userDao, userFlow, throwableFlow, messageFlow, extension ) } diff --git a/common/src/main/java/dev/brahmkshatriya/echo/common/clients/MessagePostClient.kt b/common/src/main/java/dev/brahmkshatriya/echo/common/clients/MessagePostClient.kt index 03375679..86074b76 100644 --- a/common/src/main/java/dev/brahmkshatriya/echo/common/clients/MessagePostClient.kt +++ b/common/src/main/java/dev/brahmkshatriya/echo/common/clients/MessagePostClient.kt @@ -1,5 +1,5 @@ package dev.brahmkshatriya.echo.common.clients -abstract class MessagePostClient { - var postMessage: (String) -> Unit = {} +interface MessagePostClient { + var postMessage: (String) -> Unit } \ No newline at end of file From d7ad23fdef7a61e0d664418f40e51042ba3468de Mon Sep 17 00:00:00 2001 From: rebel onion <87634197+rebelonion@users.noreply.github.com> Date: Thu, 21 Nov 2024 00:15:16 -0600 Subject: [PATCH 11/18] fix: better message client (again) --- .../dev/brahmkshatriya/echo/extensions/ExtensionLoader.kt | 4 ++-- .../brahmkshatriya/echo/common/clients/MessagePostClient.kt | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/dev/brahmkshatriya/echo/extensions/ExtensionLoader.kt b/app/src/main/java/dev/brahmkshatriya/echo/extensions/ExtensionLoader.kt index dec73ad8..f3a2396d 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/extensions/ExtensionLoader.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/extensions/ExtensionLoader.kt @@ -323,8 +323,8 @@ class ExtensionLoader( scope: CoroutineScope, mutableMessageFlow: MutableSharedFlow ) { - client.postMessage = { message -> - scope.launch(Dispatchers.Main) { + client.setMessageHandler { message -> + scope.launch(Dispatchers.Main.immediate) { mutableMessageFlow.emit(SnackBar.Message(message)) } } diff --git a/common/src/main/java/dev/brahmkshatriya/echo/common/clients/MessagePostClient.kt b/common/src/main/java/dev/brahmkshatriya/echo/common/clients/MessagePostClient.kt index 86074b76..4247d7d8 100644 --- a/common/src/main/java/dev/brahmkshatriya/echo/common/clients/MessagePostClient.kt +++ b/common/src/main/java/dev/brahmkshatriya/echo/common/clients/MessagePostClient.kt @@ -1,5 +1,6 @@ package dev.brahmkshatriya.echo.common.clients interface MessagePostClient { - var postMessage: (String) -> Unit + fun postMessage(message: String) + fun setMessageHandler(handler: (String) -> Unit) } \ No newline at end of file From e7dbf69b21afe73e508e433651739222a0e06a5d Mon Sep 17 00:00:00 2001 From: rebel onion <87634197+rebelonion@users.noreply.github.com> Date: Thu, 21 Nov 2024 03:24:25 -0600 Subject: [PATCH 12/18] feat: better message viewing --- .../echo/extensions/ControllerExtensionService.kt | 1 + .../echo/extensions/ExtensionLoader.kt | 15 +++++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/dev/brahmkshatriya/echo/extensions/ControllerExtensionService.kt b/app/src/main/java/dev/brahmkshatriya/echo/extensions/ControllerExtensionService.kt index b5f1d3bc..ff236ddb 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/extensions/ControllerExtensionService.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/extensions/ControllerExtensionService.kt @@ -107,6 +107,7 @@ class ControllerExtensionService : Service() { } override fun onDestroy() { + stopForeground(STOP_FOREGROUND_DETACH) player = null super.onDestroy() } diff --git a/app/src/main/java/dev/brahmkshatriya/echo/extensions/ExtensionLoader.kt b/app/src/main/java/dev/brahmkshatriya/echo/extensions/ExtensionLoader.kt index f3a2396d..76242cc1 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/extensions/ExtensionLoader.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/extensions/ExtensionLoader.kt @@ -269,6 +269,8 @@ class ExtensionLoader( const val LAST_EXTENSION_KEY = "last_extension" private const val TIMEOUT = 5000L + private val messageScope = MainScope() + CoroutineName("MessageHandler") + fun ExtensionType.priorityKey() = "priority_$this" fun setupMusicExtension( @@ -296,8 +298,9 @@ class ExtensionLoader( mutableMessageFlow: MutableSharedFlow, extension: Extension<*>, ) = withContext(Dispatchers.IO) { + extension.takeIf { it.metadata.enabled } ?: return@withContext extension.get(throwableFlow){ - registerMessagePostClient(this, this@withContext, mutableMessageFlow) + registerMessagePostClient(this, extension.name, mutableMessageFlow) } extension.run(throwableFlow) { withTimeout(TIMEOUT) { onExtensionSelected() } @@ -320,12 +323,16 @@ class ExtensionLoader( private fun registerMessagePostClient( client: MessagePostClient, - scope: CoroutineScope, + name: String, mutableMessageFlow: MutableSharedFlow ) { client.setMessageHandler { message -> - scope.launch(Dispatchers.Main.immediate) { - mutableMessageFlow.emit(SnackBar.Message(message)) + messageScope.launch(Dispatchers.Main) { + mutableMessageFlow.emit( + SnackBar.Message( + "$name: $message", + ) + ) } } } From e48423c18c80396761a6c778b27926ed827822b9 Mon Sep 17 00:00:00 2001 From: rebelonion <87634197+rebelonion@users.noreply.github.com> Date: Thu, 21 Nov 2024 19:07:26 -0600 Subject: [PATCH 13/18] proposal --- .../java/dev/brahmkshatriya/echo/PlayerService.kt | 11 +++++++++++ .../dev/brahmkshatriya/echo/di/ExtensionModule.kt | 7 +++++++ .../echo/extensions/ExtensionLoader.kt | 13 ++++++++++--- .../echo/viewmodels/ExtensionViewModel.kt | 4 +++- .../echo/common/clients/CloseableClient.kt | 5 +++++ 5 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 common/src/main/java/dev/brahmkshatriya/echo/common/clients/CloseableClient.kt diff --git a/app/src/main/java/dev/brahmkshatriya/echo/PlayerService.kt b/app/src/main/java/dev/brahmkshatriya/echo/PlayerService.kt index 3e66bd1c..ad28dabf 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/PlayerService.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/PlayerService.kt @@ -17,6 +17,7 @@ import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaSession import dagger.hilt.android.AndroidEntryPoint import dev.brahmkshatriya.echo.common.MusicExtension +import dev.brahmkshatriya.echo.common.clients.CloseableClient import dev.brahmkshatriya.echo.common.models.Streamable import dev.brahmkshatriya.echo.extensions.ExtensionLoader import dev.brahmkshatriya.echo.playback.Current @@ -58,6 +59,9 @@ class PlayerService : MediaLibraryService() { @Inject lateinit var stateFlow: MutableStateFlow + @Inject + lateinit var closeableFlow: MutableStateFlow?> + @Inject lateinit var settings: SharedPreferences @@ -182,6 +186,13 @@ class PlayerService : MediaLibraryService() { release() mediaSession = null } + closeableFlow.value?.forEach { + try { + it.close() + } catch (e: Exception) { + throwFlow.tryEmit(e) + } + } if (::controllerListener.isInitialized) controllerListener.onDestroy() super.onDestroy() } diff --git a/app/src/main/java/dev/brahmkshatriya/echo/di/ExtensionModule.kt b/app/src/main/java/dev/brahmkshatriya/echo/di/ExtensionModule.kt index a29346f7..2a24b5db 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/di/ExtensionModule.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/di/ExtensionModule.kt @@ -14,6 +14,7 @@ import dev.brahmkshatriya.echo.common.ControllerExtension import dev.brahmkshatriya.echo.common.LyricsExtension import dev.brahmkshatriya.echo.common.MusicExtension import dev.brahmkshatriya.echo.common.TrackerExtension +import dev.brahmkshatriya.echo.common.clients.CloseableClient import dev.brahmkshatriya.echo.db.models.UserEntity import dev.brahmkshatriya.echo.extensions.ExtensionLoader import dev.brahmkshatriya.echo.offline.OfflineExtension @@ -56,6 +57,10 @@ class ExtensionModule { @Singleton fun provideControllerListFlow() = MutableStateFlow?>(null) + @Provides + @Singleton + fun provideCloseableClientListFlow() = MutableStateFlow?>(null) + @Provides @Singleton fun provideExtensionLoader( @@ -72,6 +77,7 @@ class ExtensionModule { controllerListFlow: MutableStateFlow?>, lyricsListFlow: MutableStateFlow?>, extensionFlow: MutableStateFlow, + closeableClientListFlow: MutableStateFlow?>, ) = run { val extensionDao = database.extensionDao() val userDao = database.userDao() @@ -90,6 +96,7 @@ class ExtensionModule { controllerListFlow, lyricsListFlow, extensionFlow, + closeableClientListFlow ) } } \ No newline at end of file diff --git a/app/src/main/java/dev/brahmkshatriya/echo/extensions/ExtensionLoader.kt b/app/src/main/java/dev/brahmkshatriya/echo/extensions/ExtensionLoader.kt index 76242cc1..ebb1774d 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/extensions/ExtensionLoader.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/extensions/ExtensionLoader.kt @@ -7,6 +7,7 @@ import dev.brahmkshatriya.echo.common.Extension import dev.brahmkshatriya.echo.common.LyricsExtension import dev.brahmkshatriya.echo.common.MusicExtension import dev.brahmkshatriya.echo.common.TrackerExtension +import dev.brahmkshatriya.echo.common.clients.CloseableClient import dev.brahmkshatriya.echo.common.clients.ExtensionClient import dev.brahmkshatriya.echo.common.clients.LoginClient import dev.brahmkshatriya.echo.common.clients.MessagePostClient @@ -60,6 +61,7 @@ class ExtensionLoader( private val controllerListFlow: MutableStateFlow?>, private val lyricsListFlow: MutableStateFlow?>, private val extensionFlow: MutableStateFlow, + private val closeableFLow: MutableStateFlow?>, ) { private val scope = MainScope() + CoroutineName("ExtensionLoader") private val listener = PackageChangeListener(context) @@ -222,7 +224,7 @@ class ExtensionLoader( val id = settings.getString(LAST_EXTENSION_KEY, null) val extension = extensions.find { it.metadata.id == id } ?: extensions.firstOrNull() setupMusicExtension( - scope, settings, extensionFlow, userDao, userFlow, throwableFlow, mutableMessageFlow, extension + scope, settings, extensionFlow, userDao, userFlow, throwableFlow, mutableMessageFlow, closeableFLow, extension ) refresher.emit(false) music.emit(Unit) @@ -254,7 +256,7 @@ class ExtensionLoader( private suspend fun List>.setExtensions() = coroutineScope { map { async { - setExtension(userDao, userFlow, throwableFlow, mutableMessageFlow, it) + setExtension(userDao, userFlow, throwableFlow, mutableMessageFlow, closeableFLow, it) } }.awaitAll() } @@ -281,12 +283,13 @@ class ExtensionLoader( userFlow: MutableSharedFlow, throwableFlow: MutableSharedFlow, mutableMessageFlow: MutableSharedFlow, + closeableFlow: MutableStateFlow?>, extension: MusicExtension? ) { settings.edit().putString(LAST_EXTENSION_KEY, extension?.id).apply() extension?.takeIf { it.metadata.enabled } ?: return scope.launch { - setExtension(userDao, userFlow, throwableFlow, mutableMessageFlow, extension) + setExtension(userDao, userFlow, throwableFlow, mutableMessageFlow, closeableFlow, extension) extensionFlow.value = extension } } @@ -296,12 +299,16 @@ class ExtensionLoader( userFlow: MutableSharedFlow, throwableFlow: MutableSharedFlow, mutableMessageFlow: MutableSharedFlow, + closeableFlow: MutableStateFlow?>, extension: Extension<*>, ) = withContext(Dispatchers.IO) { extension.takeIf { it.metadata.enabled } ?: return@withContext extension.get(throwableFlow){ registerMessagePostClient(this, extension.name, mutableMessageFlow) } + extension.get(throwableFlow) { + closeableFlow.value = closeableFlow.value.orEmpty() + this + } extension.run(throwableFlow) { withTimeout(TIMEOUT) { onExtensionSelected() } } diff --git a/app/src/main/java/dev/brahmkshatriya/echo/viewmodels/ExtensionViewModel.kt b/app/src/main/java/dev/brahmkshatriya/echo/viewmodels/ExtensionViewModel.kt index cc4e6766..ff3cd725 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/viewmodels/ExtensionViewModel.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/viewmodels/ExtensionViewModel.kt @@ -19,6 +19,7 @@ import dev.brahmkshatriya.echo.common.Extension import dev.brahmkshatriya.echo.common.LyricsExtension import dev.brahmkshatriya.echo.common.MusicExtension import dev.brahmkshatriya.echo.common.TrackerExtension +import dev.brahmkshatriya.echo.common.clients.CloseableClient import dev.brahmkshatriya.echo.common.clients.SettingsChangeListenerClient import dev.brahmkshatriya.echo.common.helpers.ExtensionType import dev.brahmkshatriya.echo.common.helpers.ImportType @@ -64,6 +65,7 @@ class ExtensionViewModel @Inject constructor( val trackerListFlow: MutableStateFlow?>, val lyricsListFlow: MutableStateFlow?>, val controllerListFlow: MutableStateFlow?>, + private val closeableFlow: MutableStateFlow?>, val extensionFlow: MutableStateFlow, val settings: SharedPreferences, val database: EchoDatabase, @@ -86,7 +88,7 @@ class ExtensionViewModel @Inject constructor( private val userDao = database.userDao() fun setExtension(extension: MusicExtension?) { setupMusicExtension( - viewModelScope, settings, extensionFlow, userDao, userFlow, throwableFlow, messageFlow, extension + viewModelScope, settings, extensionFlow, userDao, userFlow, throwableFlow, messageFlow, closeableFlow, extension ) } diff --git a/common/src/main/java/dev/brahmkshatriya/echo/common/clients/CloseableClient.kt b/common/src/main/java/dev/brahmkshatriya/echo/common/clients/CloseableClient.kt new file mode 100644 index 00000000..0759de53 --- /dev/null +++ b/common/src/main/java/dev/brahmkshatriya/echo/common/clients/CloseableClient.kt @@ -0,0 +1,5 @@ +package dev.brahmkshatriya.echo.common.clients + +interface CloseableClient { + fun close() +} \ No newline at end of file From ce51a2a9b407bd9762c507a2f5e57dc96dfa77bb Mon Sep 17 00:00:00 2001 From: rebelonion <87634197+rebelonion@users.noreply.github.com> Date: Thu, 21 Nov 2024 20:05:14 -0600 Subject: [PATCH 14/18] fix: remove unnecessary atomic --- .../playback/listeners/ControllerListener.kt | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/dev/brahmkshatriya/echo/playback/listeners/ControllerListener.kt b/app/src/main/java/dev/brahmkshatriya/echo/playback/listeners/ControllerListener.kt index faa85be5..29a03b8b 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/playback/listeners/ControllerListener.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/playback/listeners/ControllerListener.kt @@ -22,7 +22,6 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import java.util.concurrent.atomic.AtomicBoolean @UnstableApi class ControllerListener( @@ -35,7 +34,7 @@ class ControllerListener( private var audioManager: AudioManager = service.getSystemService(Context.AUDIO_SERVICE) as AudioManager private val serviceHelper = ControllerServiceHelper(service) - private val needsService: AtomicBoolean = AtomicBoolean(false) + private var needsService: Boolean = false init { scope.launch { @@ -59,14 +58,14 @@ class ControllerListener( private suspend fun registerController(extension: ControllerExtension) { extension.get(throwableFlow) { if (runsDuringPause) { - if (!needsService.get()) { + if (!needsService) { serviceHelper.startService(player) } - needsService.set(true) + needsService = true } onPlayRequest = { tryOnMain(Player.COMMAND_PLAY_PAUSE) { - if (needsService.get()) { + if (needsService) { serviceHelper.play() } else { player.play() @@ -75,7 +74,7 @@ class ControllerListener( } onPauseRequest = { tryOnMain(Player.COMMAND_PLAY_PAUSE) { - if (needsService.get()) { + if (needsService) { serviceHelper.pause() } else { player.pause() @@ -84,7 +83,7 @@ class ControllerListener( } onNextRequest = { tryOnMain(Player.COMMAND_SEEK_TO_NEXT) { - if (needsService.get()) { + if (needsService) { serviceHelper.seekToNext() } else { player.seekToNextMediaItem() @@ -93,7 +92,7 @@ class ControllerListener( } onPreviousRequest = { tryOnMain(Player.COMMAND_SEEK_TO_PREVIOUS) { - if (needsService.get()) { + if (needsService) { serviceHelper.seekToPrevious() } else { player.seekToPreviousMediaItem() @@ -102,7 +101,7 @@ class ControllerListener( } onSeekRequest = { position -> tryOnMain(Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM) { - if (needsService.get()) { + if (needsService) { serviceHelper.seekTo(position) } else { player.seekTo(position) @@ -111,7 +110,7 @@ class ControllerListener( } onSeekToMediaItemRequest = { index -> tryOnMain(Player.COMMAND_SEEK_TO_MEDIA_ITEM) { - if (needsService.get()) { + if (needsService) { serviceHelper.seekToMediaItem(index) } else { player.seekTo(index, 0) @@ -120,7 +119,7 @@ class ControllerListener( } onMovePlaylistItemRequest = { fromIndex, toIndex -> tryOnMain(Player.COMMAND_CHANGE_MEDIA_ITEMS) { - if (needsService.get()) { + if (needsService) { serviceHelper.moveMediaItem(fromIndex, toIndex) } else { player.moveMediaItem(fromIndex, toIndex) @@ -129,7 +128,7 @@ class ControllerListener( } onRemovePlaylistItemRequest = { index -> tryOnMain(Player.COMMAND_CHANGE_MEDIA_ITEMS) { - if (needsService.get()) { + if (needsService) { serviceHelper.removeMediaItem(index) } else { player.removeMediaItem(index) @@ -138,7 +137,7 @@ class ControllerListener( } onShuffleModeRequest = { enabled -> tryOnMain(Player.COMMAND_SET_SHUFFLE_MODE) { - if (needsService.get()) { + if (needsService) { serviceHelper.setShuffleMode(enabled) } else { player.shuffleModeEnabled = enabled @@ -147,7 +146,7 @@ class ControllerListener( } onRepeatModeRequest = { repeatMode -> tryOnMain(Player.COMMAND_SET_REPEAT_MODE) { - if (needsService.get()) { + if (needsService) { serviceHelper.setRepeatMode(repeatMode) } else { player.repeatMode = repeatMode.ordinal From fedf229e9f73a3e24745465cbe9feb08acf8a86e Mon Sep 17 00:00:00 2001 From: rebel onion <87634197+rebelonion@users.noreply.github.com> Date: Sat, 23 Nov 2024 02:32:59 -0600 Subject: [PATCH 15/18] feat: docs (crazy, I know) --- .../echo/common/clients/CloseableClient.kt | 4 ++++ .../echo/common/clients/MessagePostClient.kt | 11 +++++++++++ 2 files changed, 15 insertions(+) diff --git a/common/src/main/java/dev/brahmkshatriya/echo/common/clients/CloseableClient.kt b/common/src/main/java/dev/brahmkshatriya/echo/common/clients/CloseableClient.kt index 0759de53..903ff1ae 100644 --- a/common/src/main/java/dev/brahmkshatriya/echo/common/clients/CloseableClient.kt +++ b/common/src/main/java/dev/brahmkshatriya/echo/common/clients/CloseableClient.kt @@ -1,5 +1,9 @@ package dev.brahmkshatriya.echo.common.clients interface CloseableClient { + /** + * Called when the app and player are closed. + * Useful for cleaning up resources. + */ fun close() } \ No newline at end of file diff --git a/common/src/main/java/dev/brahmkshatriya/echo/common/clients/MessagePostClient.kt b/common/src/main/java/dev/brahmkshatriya/echo/common/clients/MessagePostClient.kt index 4247d7d8..814e98d1 100644 --- a/common/src/main/java/dev/brahmkshatriya/echo/common/clients/MessagePostClient.kt +++ b/common/src/main/java/dev/brahmkshatriya/echo/common/clients/MessagePostClient.kt @@ -1,6 +1,17 @@ package dev.brahmkshatriya.echo.common.clients interface MessagePostClient { + /** + * Posts a message to the user. + * Call this when you want to display a notification to the user. + * + * @param message The message text to display + */ fun postMessage(message: String) + + /** + * Internal setup method used by the main app. + * Extensions should not call this method. + */ fun setMessageHandler(handler: (String) -> Unit) } \ No newline at end of file From 3d7a71e0f68c847aeddedd23f15bb4a2ed715064 Mon Sep 17 00:00:00 2001 From: rebel onion <87634197+rebelonion@users.noreply.github.com> Date: Sat, 23 Nov 2024 02:39:58 -0600 Subject: [PATCH 16/18] fix: update text setting after setting value --- .../echo/utils/prefs/MaterialTextInputPreference.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/dev/brahmkshatriya/echo/utils/prefs/MaterialTextInputPreference.kt b/app/src/main/java/dev/brahmkshatriya/echo/utils/prefs/MaterialTextInputPreference.kt index e2781c7a..8b83a5a3 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/utils/prefs/MaterialTextInputPreference.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/utils/prefs/MaterialTextInputPreference.kt @@ -35,6 +35,7 @@ class MaterialTextInputPreference(context: Context) : EditTextPreference(context val newText = editText?.text?.toString() if (callChangeListener(newText)) { text = newText + updateSummary() dialog.dismiss() } } From a9bd9b453774f33b0a927b4515e4b50e0fe393d2 Mon Sep 17 00:00:00 2001 From: rebel onion <87634197+rebelonion@users.noreply.github.com> Date: Sat, 23 Nov 2024 03:05:20 -0600 Subject: [PATCH 17/18] fix: remove unnecessary parentheses --- .../echo/playback/listeners/ControllerListener.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/dev/brahmkshatriya/echo/playback/listeners/ControllerListener.kt b/app/src/main/java/dev/brahmkshatriya/echo/playback/listeners/ControllerListener.kt index 29a03b8b..9c898f91 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/playback/listeners/ControllerListener.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/playback/listeners/ControllerListener.kt @@ -225,7 +225,7 @@ class ControllerListener( private fun getVolume(): Double { val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) val volume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) - return (volume.toDouble() / maxVolume.toDouble()) + return volume.toDouble() / maxVolume.toDouble() } From 8cf32f54e7493ed5d9d8c319c4ccfd0513393c9d Mon Sep 17 00:00:00 2001 From: rebel onion <87634197+rebelonion@users.noreply.github.com> Date: Sat, 23 Nov 2024 18:28:26 -0600 Subject: [PATCH 18/18] fix: only register enabled extensions --- .../echo/playback/listeners/ControllerListener.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/dev/brahmkshatriya/echo/playback/listeners/ControllerListener.kt b/app/src/main/java/dev/brahmkshatriya/echo/playback/listeners/ControllerListener.kt index 9c898f91..8c97d92e 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/playback/listeners/ControllerListener.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/playback/listeners/ControllerListener.kt @@ -39,7 +39,7 @@ class ControllerListener( init { scope.launch { controllerExtensions.collect { extensions -> - extensions?.forEach { + extensions?.filter { it.metadata.enabled }?.forEach { launch { registerController(it) }