-
-
Notifications
You must be signed in to change notification settings - Fork 6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Feature] Android Auto #71
Comments
Yes, Android Auto support is one of the long-term goals for the app. To be honest, I don’t have much idea how users interact with Android Auto, as I’m just an average European who doesn’t drive at all—even on quiet roads. If you could share your personal scenarios for using audiobooks and podcasts while driving, that would be the best gift for me. |
Sure thing! You can take Voice (https://github.com/PaulWoitaschek/Voice) as an example... I will try to describe it, and if not, I will fire up the headunit developer emulator and get you some screenshots... Basically a book selection screen with the books you want can listen, and and a player screen with the cover, time progressbar, and buttons (next chapter or next 10s, previous chapter or previous 10s, play/pause), this would suffice... Additionally I have been using plappa (for ios) and in carplay they have another screen with chapter selection... can be the same as book selection once you have selected a book, but I have been using voice for a while, and its not that important... In fact, once you select a book in the phone, the player screen is all that really matters. But I feel that a book selection is required anyways... Let me search for screenshots for you, if not, during the week I will try and make them myself. ps: Spaniard here :), but I do drive regularly from my city to my parents city (4h+ commute), ideal for books :) |
ok, got it. Thanks, I definitely need to check out how similar apps provide UI/UX |
I played yesterday with adding the Android Auto... but after Java... I went for Flutter, so my multi-routine kotlin is not rusted... its non-existent! This being said... I managed to make the app apear in the headunit emulator... but I have some questions... Mainly, how do you use the The thing is, on one hand, there are not much options for creating android auto UI, For what I found (via the wizzard) there are 2 options, map or audio screens. For audio screens, its already predefined and via inheritance and overriding of methods. There is one for populating the list, some for playback controls... etc... In the end it comes down to convert somehow the Book entity into the MediaItem entity... wich is playable by the android auto activity. Coming to what I said in the begining... this methods are already overriden, and the parent method does not 'suspend'... So I'm out of luck when trying to ask for libraries, picking the first one, and asking for all the books. Also the ApiResult is a little bit pain to use (if im using it right) as, after creating an auxiliary method that could 'suspend', I was not sure of what method of ApiResult to use... fold? for each subsequent call? Also, how is your Dependency Injector done? I tried to add the Provider via the constructor, but without any luck. With this solved, I could try and test the previous things with a 'GlobalScope' (I found in the internet) and continue testing and trying... If I make it work, I will propose a PR :) |
Hi! First of all, thank you for your efforts. Let me try to answer your questions about the code. About suspend To handle asynchronous code, you can look at how it's done in Jetpack Compose UI, where suspend methods are run within CoroutineScope.launch{} or similar wrappers. You might be tempted to use runBlocking{}, but this is not an option, as it would block the thread and cause visible app freezes. About fold and Applicative Monad Transformations You can learn more about monadic transformations here: https://typelevel.org/cats/typeclasses/foldable.html. Essentially, ApiResult is an implementation of the "Either" pattern, where a method can return not just a result but a container holding mutually exclusive options: either an error (left) or data (right). ApiResult is designed to be processed via a chain of map/flatMap, like any functor/applicative (which is a subset of a monad). When you need to handle side effects, you'll use .fold to resolve the container and explicitly process both scenarios (error and success). For more practical applications of monads, I recommend reading the Red Book by Odersky. While it focuses on Scala, it reflects the modern approach to functional programming in the JVM ecosystem, of which Kotlin is a part. About Dependency Injection (DI) You can find more details about creating such singletons here: https://rommansabbir.com/mastering-hilt-android-kotlin-hilt-dagger-2-part-3. However, you can generally follow the existing approach in the application. |
Wow! Thank you for the info! I will try this out for sure! |
package org.grakovne.lissen
import android.os.Bundle
import android.support.v4.media.MediaBrowserCompat.MediaItem
import android.support.v4.media.MediaDescriptionCompat
import androidx.media.MediaBrowserServiceCompat
import android.support.v4.media.session.MediaSessionCompat
import android.util.Log
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import org.grakovne.lissen.content.LissenMediaProvider
import org.grakovne.lissen.domain.Book
import org.grakovne.lissen.domain.DetailedItem
import org.grakovne.lissen.domain.Library
import org.grakovne.lissen.widget.MediaRepository
import java.util.ArrayList
import javax.inject.Inject
@AndroidEntryPoint
class AutoBookPlayerService : MediaBrowserServiceCompat() {
private lateinit var session: MediaSessionCompat
@Inject
lateinit var mediaChannel: LissenMediaProvider
@Inject
lateinit var mediaRepository: MediaRepository
private val callback = object : MediaSessionCompat.Callback() {
override fun onPlay() {
Log.d(TAG, "onPlay?")
}
override fun onSkipToQueueItem(queueId: Long) {}
override fun onSeekTo(position: Long) {}
override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) {
Log.d(TAG, "onPlayFromMediaId?: $mediaId")
if(mediaId == null) return
CoroutineScope(Dispatchers.Main).launch {
mediaRepository.preparePlayback(mediaId, true)
mediaRepository.togglePlayPause()
}
}
override fun onPause() {}
override fun onStop() {}
override fun onSkipToNext() {}
override fun onSkipToPrevious() {}
override fun onCustomAction(action: String?, extras: Bundle?) {}
override fun onPlayFromSearch(query: String?, extras: Bundle?) {}
}
override fun onCreate() {
super.onCreate()
session = MediaSessionCompat(this, "AutoBookPlayerService")
sessionToken = session.sessionToken
session.setCallback(callback)
session.setFlags(
MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or
MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS
)
}
override fun onDestroy() {
session.release()
}
override fun onGetRoot(
clientPackageName: String,
clientUid: Int,
rootHints: Bundle?
): BrowserRoot? {
return BrowserRoot("root", null)
}
override fun onLoadChildren(parentId: String, result: Result<MutableList<MediaItem>>) {
CoroutineScope(Dispatchers.Main).launch {
result.sendResult(getBooksMediaItems())
}
result.detach()
}
suspend fun getDetailedBook(id: String): DetailedItem? {
return mediaChannel.fetchBook(id).fold(onFailure = { null }, onSuccess = { it })
}
suspend fun getBooksMediaItems(): MutableList<MediaItem>{
mediaChannel.fetchLibraries().fold( onSuccess = {it.first()}, onFailure = {null})?.let { library ->
val books = mediaChannel.fetchBooks(library.id,4,1)
return books
.fold(onSuccess = { pagedItems -> pagedItems.items.map {
val desc = MediaDescriptionCompat.Builder()
.setMediaId(it.id)
.setTitle(it.title)
.setSubtitle(it.author)
.build()
MediaItem(desc, MediaItem.FLAG_PLAYABLE)
} }, onFailure = { listOf() })
.toMutableList()
}
return mutableListOf()
}
companion object{
private const val TAG: String = "AutoBookPlayerService"
}
} <!--
Main music service, provides media browsing and media playback services to
consumers through MediaBrowserService and MediaSession. Consumers connect to it through
MediaBrowser (for browsing) and MediaController (for playback control)
-->
<service
android:name=".AutoBookPlayerService"
android:exported="true">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service> It does something... still figuring out the media session thing, as I believe that your playbackservice can be reuse enterily... |
hat's a great start, but let me show several issues with the code design: MediaBrowserServiceCompat is a legacy component. The app is trying to work on the Media3 stack: https://developer.android.com/media/implement/playback-app. Mixing MediaBrowserServiceCompat and Media3 is a bad idea because they do not interoperate with each other, and we can't rely on automatic data transfer between them. CoroutineScope(Dispatchers.Main) should be avoided when preparing a book. This might be a heavy operation, and it will freeze the UI until it’s complete. Consider using the IO scope to offload the task from the main UI thread. suspend fun getBooksMediaItems(): MutableList is the main issue here. I'll approve the changes if you decide to give up and can't handle this properly, but let’s try to fix it first. By the way, could you open an MR so we can discuss it there? |
Here it is! #76 |
Just want to say that I would also love this feature to be included! I have over an hour commute and would really like to use this app to listen to my audio books on my server. Thanks for everyone's effort! |
Another vote for Android Auto support. Thank you all for the work you do. |
@GrakovNe ... You have far more experience than me with Kotlin/Media3... feel free to take the PR over and finish it at any time :) |
Is there plans to include Android Auto?
Thank you for the awesome Job!!
The text was updated successfully, but these errors were encountered: