From a1ab8824c389ddf5d159a9409cf420ef2911b8d2 Mon Sep 17 00:00:00 2001 From: Andrey Gusev Date: Sun, 10 Dec 2023 15:55:02 +0300 Subject: [PATCH 1/5] Relax the controller check on onConnect This enables external controllers other than android auto to resume the playback --- .../voice/playback/session/LibrarySessionCallback.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/playback/src/main/kotlin/voice/playback/session/LibrarySessionCallback.kt b/playback/src/main/kotlin/voice/playback/session/LibrarySessionCallback.kt index 5f705baff8..1d7ed54a8e 100644 --- a/playback/src/main/kotlin/voice/playback/session/LibrarySessionCallback.kt +++ b/playback/src/main/kotlin/voice/playback/session/LibrarySessionCallback.kt @@ -33,6 +33,10 @@ import voice.playback.session.search.BookSearchHandler import voice.playback.session.search.BookSearchParser import javax.inject.Inject + +private const val APP_PACKAGE_ID = "de.ph1b.audiobook" + + class LibrarySessionCallback @Inject constructor( private val mediaItemProvider: MediaItemProvider, @@ -166,9 +170,7 @@ class LibrarySessionCallback ): ConnectionResult { Logger.d("onConnect to ${controller.packageName}") - if (player.playbackState == Player.STATE_IDLE && - controller.packageName == "com.google.android.projection.gearhead" - ) { + if (player.playbackState == Player.STATE_IDLE && controller.packageName != APP_PACKAGE_ID) { Logger.d("onConnect to ${controller.packageName} and player is idle.") Logger.d("Preparing current book so it shows up as recently played") scope.launch { From bc6ef8631a58e539491f0dc3891143faf6895a45 Mon Sep 17 00:00:00 2001 From: Andrey Gusev Date: Sun, 10 Dec 2023 15:59:03 +0300 Subject: [PATCH 2/5] Add new session creation if the is released After an external playback start, app launch and exit the session can be left in the released state until process finish --- .../voice/playback/session/PlaybackService.kt | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/playback/src/main/kotlin/voice/playback/session/PlaybackService.kt b/playback/src/main/kotlin/voice/playback/session/PlaybackService.kt index 5f66c76b93..3fc1117733 100644 --- a/playback/src/main/kotlin/voice/playback/session/PlaybackService.kt +++ b/playback/src/main/kotlin/voice/playback/session/PlaybackService.kt @@ -57,14 +57,13 @@ class PlaybackService : MediaLibraryService() { release() } - override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaLibrarySession? { - return session.takeUnless { session -> - session.invokeIsReleased - }.also { - if (it == null) { - Logger.e("onGetSession returns null because the session is already released") - } - } + override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaLibrarySession { + if (session.invokeIsReleased) + rootComponentAs() + .playbackComponentFactory + .create(this) + .inject(this) + return session } } From bbecb23f6f58823345d92a188af25bd9a3de7418 Mon Sep 17 00:00:00 2001 From: Andrey Gusev Date: Sun, 4 Feb 2024 15:05:29 +0300 Subject: [PATCH 3/5] Move the position loading Instead of loading the playback position on any external controller connection it is now loaded on service creation --- .../voice/playback/player/VoicePlayer.kt | 8 ++++++++ .../playback/session/LibrarySessionCallback.kt | 18 ++++-------------- .../voice/playback/session/PlaybackService.kt | 4 ++++ 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/playback/src/main/kotlin/voice/playback/player/VoicePlayer.kt b/playback/src/main/kotlin/voice/playback/player/VoicePlayer.kt index aa0c51af38..807d3476c5 100644 --- a/playback/src/main/kotlin/voice/playback/player/VoicePlayer.kt +++ b/playback/src/main/kotlin/voice/playback/player/VoicePlayer.kt @@ -322,6 +322,14 @@ class VoicePlayer val bookId = currentBookId.data.first() ?: return repo.updateBook(bookId, update) } + + suspend fun prepareCurrentBook() { + val bookId = currentBookId.data.first() ?: return + val book = repo.get(bookId) ?: return + val item = mediaItemProvider.mediaItem(book) + setMediaItem(item) + prepare() + } } private const val THRESHOLD_FOR_BACK_SEEK_MS = 2000 diff --git a/playback/src/main/kotlin/voice/playback/session/LibrarySessionCallback.kt b/playback/src/main/kotlin/voice/playback/session/LibrarySessionCallback.kt index 1cfb4f9878..c77b89f0b5 100644 --- a/playback/src/main/kotlin/voice/playback/session/LibrarySessionCallback.kt +++ b/playback/src/main/kotlin/voice/playback/session/LibrarySessionCallback.kt @@ -32,10 +32,6 @@ import voice.playback.session.search.BookSearchHandler import voice.playback.session.search.BookSearchParser import javax.inject.Inject - -private const val APP_PACKAGE_ID = "de.ph1b.audiobook" - - class LibrarySessionCallback @Inject constructor( private val mediaItemProvider: MediaItemProvider, @@ -168,11 +164,13 @@ class LibrarySessionCallback ): ConnectionResult { Logger.d("onConnect to ${controller.packageName}") - if (player.playbackState == Player.STATE_IDLE && controller.packageName != APP_PACKAGE_ID) { + if (player.playbackState == Player.STATE_IDLE && + controller.packageName == "com.google.android.projection.gearhead" + ) { Logger.d("onConnect to ${controller.packageName} and player is idle.") Logger.d("Preparing current book so it shows up as recently played") scope.launch { - prepareCurrentBook() + player.prepareCurrentBook() } } @@ -190,14 +188,6 @@ class LibrarySessionCallback ) } - private suspend fun prepareCurrentBook() { - val bookId = currentBookId.data.first() ?: return - val book = bookRepository.get(bookId) ?: return - val item = mediaItemProvider.mediaItem(book) - player.setMediaItem(item) - player.prepare() - } - override fun onCustomCommand( session: MediaSession, controller: ControllerInfo, diff --git a/playback/src/main/kotlin/voice/playback/session/PlaybackService.kt b/playback/src/main/kotlin/voice/playback/session/PlaybackService.kt index 3fc1117733..807a86c376 100644 --- a/playback/src/main/kotlin/voice/playback/session/PlaybackService.kt +++ b/playback/src/main/kotlin/voice/playback/session/PlaybackService.kt @@ -5,6 +5,7 @@ import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaSession import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch import voice.common.rootComponentAs import voice.logging.core.Logger import voice.playback.di.PlaybackComponent @@ -32,6 +33,9 @@ class PlaybackService : MediaLibraryService() { .create(this) .inject(this) setMediaNotificationProvider(voiceNotificationProvider) + scope.launch { + player.prepareCurrentBook() + } } override fun onTaskRemoved(rootIntent: Intent?) { From fbe0efaae13bd3c7b14ad2f63e2f777034b9bc3d Mon Sep 17 00:00:00 2001 From: Andrey Gusev Date: Mon, 5 Feb 2024 19:28:39 +0300 Subject: [PATCH 4/5] Return the current book for empty query searches Legacy controllers have onPlayFromSearch method that is called for play button and expected to play something meaningful. For media3 sessions it delegates to getSearchResult. In this app search was already returning the last position for calls with null as query, but in the mentioned scenario it is called with an empty string, so, instead of manually loading the last book played it is now returned for empty query searches --- .../main/kotlin/voice/playback/player/VoicePlayer.kt | 8 -------- .../voice/playback/session/LibrarySessionCallback.kt | 10 +++++++++- .../kotlin/voice/playback/session/PlaybackService.kt | 4 ---- .../voice/playback/session/search/BookSearchHandler.kt | 2 +- 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/playback/src/main/kotlin/voice/playback/player/VoicePlayer.kt b/playback/src/main/kotlin/voice/playback/player/VoicePlayer.kt index 807d3476c5..aa0c51af38 100644 --- a/playback/src/main/kotlin/voice/playback/player/VoicePlayer.kt +++ b/playback/src/main/kotlin/voice/playback/player/VoicePlayer.kt @@ -322,14 +322,6 @@ class VoicePlayer val bookId = currentBookId.data.first() ?: return repo.updateBook(bookId, update) } - - suspend fun prepareCurrentBook() { - val bookId = currentBookId.data.first() ?: return - val book = repo.get(bookId) ?: return - val item = mediaItemProvider.mediaItem(book) - setMediaItem(item) - prepare() - } } private const val THRESHOLD_FOR_BACK_SEEK_MS = 2000 diff --git a/playback/src/main/kotlin/voice/playback/session/LibrarySessionCallback.kt b/playback/src/main/kotlin/voice/playback/session/LibrarySessionCallback.kt index c77b89f0b5..7a19891db4 100644 --- a/playback/src/main/kotlin/voice/playback/session/LibrarySessionCallback.kt +++ b/playback/src/main/kotlin/voice/playback/session/LibrarySessionCallback.kt @@ -170,7 +170,7 @@ class LibrarySessionCallback Logger.d("onConnect to ${controller.packageName} and player is idle.") Logger.d("Preparing current book so it shows up as recently played") scope.launch { - player.prepareCurrentBook() + prepareCurrentBook() } } @@ -188,6 +188,14 @@ class LibrarySessionCallback ) } + private suspend fun prepareCurrentBook() { + val bookId = currentBookId.data.first() ?: return + val book = bookRepository.get(bookId) ?: return + val item = mediaItemProvider.mediaItem(book) + player.setMediaItem(item) + player.prepare() + } + override fun onCustomCommand( session: MediaSession, controller: ControllerInfo, diff --git a/playback/src/main/kotlin/voice/playback/session/PlaybackService.kt b/playback/src/main/kotlin/voice/playback/session/PlaybackService.kt index 807a86c376..3fc1117733 100644 --- a/playback/src/main/kotlin/voice/playback/session/PlaybackService.kt +++ b/playback/src/main/kotlin/voice/playback/session/PlaybackService.kt @@ -5,7 +5,6 @@ import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaSession import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.cancel -import kotlinx.coroutines.launch import voice.common.rootComponentAs import voice.logging.core.Logger import voice.playback.di.PlaybackComponent @@ -33,9 +32,6 @@ class PlaybackService : MediaLibraryService() { .create(this) .inject(this) setMediaNotificationProvider(voiceNotificationProvider) - scope.launch { - player.prepareCurrentBook() - } } override fun onTaskRemoved(rootIntent: Intent?) { diff --git a/playback/src/main/kotlin/voice/playback/session/search/BookSearchHandler.kt b/playback/src/main/kotlin/voice/playback/session/search/BookSearchHandler.kt index ddf30b419c..3b57ef9025 100644 --- a/playback/src/main/kotlin/voice/playback/session/search/BookSearchHandler.kt +++ b/playback/src/main/kotlin/voice/playback/session/search/BookSearchHandler.kt @@ -44,7 +44,7 @@ class BookSearchHandler // Look for anything that might match the query private suspend fun searchUnstructured(query: String?): Book? { - if (query != null) { + if (query != null && query != "") { val foundMatch = findBook { val bookNameMatches = it.content.name.contains(query, ignoreCase = true) val authorMatches = it.content.author?.contains(query, ignoreCase = true) == true From fb28218f017b8eed4204149e5260b9e1cfe97f83 Mon Sep 17 00:00:00 2001 From: Andrey Gusev Date: Sat, 10 Feb 2024 20:09:34 +0300 Subject: [PATCH 5/5] Switch the search query check to isNullOrBlank --- .../kotlin/voice/playback/session/search/BookSearchHandler.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playback/src/main/kotlin/voice/playback/session/search/BookSearchHandler.kt b/playback/src/main/kotlin/voice/playback/session/search/BookSearchHandler.kt index 3b57ef9025..11d856b35f 100644 --- a/playback/src/main/kotlin/voice/playback/session/search/BookSearchHandler.kt +++ b/playback/src/main/kotlin/voice/playback/session/search/BookSearchHandler.kt @@ -44,7 +44,7 @@ class BookSearchHandler // Look for anything that might match the query private suspend fun searchUnstructured(query: String?): Book? { - if (query != null && query != "") { + if (!query.isNullOrBlank()) { val foundMatch = findBook { val bookNameMatches = it.content.name.contains(query, ignoreCase = true) val authorMatches = it.content.author?.contains(query, ignoreCase = true) == true