Skip to content

Commit

Permalink
Android Auto improvements. Icons, Start up
Browse files Browse the repository at this point in the history
Icons:
- Static browsable list items everywhere have hardcoded icons
- Libraries use icons that are defined on server

Start up / Initial loading:
- Initial loading first loads libraries from server. After that personalized shelves and items in progress are fetched simultaneously. Top menu items are update after every stage.
- If network connection is not available when android auto starts app tries to do initial loading again when network connection becomes available again
  • Loading branch information
ISO-B committed Nov 14, 2024
1 parent b335fd3 commit 802c16c
Show file tree
Hide file tree
Showing 27 changed files with 347 additions and 64 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import android.support.v4.media.MediaMetadataCompat
import androidx.media.utils.MediaConstants
import com.audiobookshelf.app.media.MediaManager
import com.fasterxml.jackson.annotation.*
import com.audiobookshelf.app.media.getUriToAbsIconDrawable

// This auto-detects whether it is a Book or Podcast
@JsonTypeInfo(use=JsonTypeInfo.Id.DEDUCTION)
Expand Down Expand Up @@ -348,7 +349,7 @@ data class Library(
var stats: LibraryStats?
) {
@JsonIgnore
fun getMediaMetadata(targetType: String? = null): MediaMetadataCompat {
fun getMediaMetadata(context: Context, targetType: String? = null): MediaMetadataCompat {
var mediaId = id
if (targetType !== null) {
mediaId = "__RECENTLY__$id"
Expand All @@ -357,6 +358,7 @@ data class Library(
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, mediaId)
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, name)
putString(MediaMetadataCompat.METADATA_KEY_TITLE, name)
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToAbsIconDrawable(context, icon).toString())
}.build()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
private var cachedLibraryDiscovery : MutableMap<String, MutableList<LibraryItem>> = hashMapOf()
private var cachedLibraryPodcasts : MutableMap<String, MutableMap<String, LibraryItem>> = hashMapOf()
private var isLibraryPodcastsCached : MutableMap<String, Boolean> = hashMapOf()
var allLibraryPersonalizationsDone : Boolean = false
private var libraryPersonalizationsDone : Int = 0

private var selectedPodcast:Podcast? = null
private var selectedLibraryItemId:String? = null
Expand Down Expand Up @@ -158,11 +160,16 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {

fun populatePersonalizedDataForAllLibraries(cb: () -> Unit ) {
serverLibraries.forEach {
libraryPersonalizationsDone++
Log.d(tag, "Loading personalization for library ${it.name} - ${it.id} - ${it.mediaType}")
populatePersonalizedDataForLibrary(it.id) {
Log.d(tag, "Loaded personalization for library ${it.name} - ${it.id} - ${it.mediaType}")
libraryPersonalizationsDone--
}
}
while (libraryPersonalizationsDone > 0) { }
allLibraryPersonalizationsDone = true
cb()
}

private fun populatePersonalizedDataForLibrary(libraryId: String, cb: () -> Unit) {
Expand All @@ -178,7 +185,9 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
if (!cachedLibraryRecentShelfs.containsKey(libraryId)) {
cachedLibraryRecentShelfs[libraryId] = mutableListOf()
}
cachedLibraryRecentShelfs[libraryId]!!.add(shelf)
if (cachedLibraryRecentShelfs[libraryId]?.find { it.id == shelf.id } == null) {
cachedLibraryRecentShelfs[libraryId]!!.add(shelf)
}
}
else if (shelf.id == "discover") {
if (!cachedLibraryDiscovery.containsKey(libraryId)) {
Expand All @@ -196,7 +205,9 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
if (!cachedLibraryRecentShelfs.containsKey(libraryId)) {
cachedLibraryRecentShelfs[libraryId] = mutableListOf()
}
cachedLibraryRecentShelfs[libraryId]!!.add(shelf)
if (cachedLibraryRecentShelfs[libraryId]?.find { it.id == shelf.id } == null) {
cachedLibraryRecentShelfs[libraryId]!!.add(shelf)
}
}
} else if (shelf.type == "episode") {
if (shelf.id == "continue-listening") return@map
Expand All @@ -205,7 +216,9 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
if (!cachedLibraryRecentShelfs.containsKey(libraryId)) {
cachedLibraryRecentShelfs[libraryId] = mutableListOf()
}
cachedLibraryRecentShelfs[libraryId]!!.add(shelf)
if (cachedLibraryRecentShelfs[libraryId]?.find { it.id == shelf.id } == null) {
cachedLibraryRecentShelfs[libraryId]!!.add(shelf)
}

(shelf as LibraryShelfEpisodeEntity).entities?.forEach { libraryItem ->
loadPodcastItem(libraryItem.libraryId, libraryItem.id) {}
Expand All @@ -216,7 +229,9 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
if (!cachedLibraryRecentShelfs.containsKey(libraryId)) {
cachedLibraryRecentShelfs[libraryId] = mutableListOf()
}
cachedLibraryRecentShelfs[libraryId]!!.add(shelf)
if (cachedLibraryRecentShelfs[libraryId]?.find { it.id == shelf.id } == null) {
cachedLibraryRecentShelfs[libraryId]!!.add(shelf)
}
}
else if (shelf.id == "discover"){
return@map
Expand All @@ -226,7 +241,9 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
if (!cachedLibraryRecentShelfs.containsKey(libraryId)) {
cachedLibraryRecentShelfs[libraryId] = mutableListOf()
}
cachedLibraryRecentShelfs[libraryId]!!.add(shelf)
if (cachedLibraryRecentShelfs[libraryId]?.find { it.id == shelf.id } == null) {
cachedLibraryRecentShelfs[libraryId]!!.add(shelf)
}
}
}

Expand Down Expand Up @@ -503,13 +520,14 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
if (!cachedLibraryDiscovery.containsKey(libraryId)) {
cb(listOf())
}
val libraryItemsWithAudio = cachedLibraryDiscovery[libraryId]!!.filter { li -> li.checkHasTracks() }
libraryItemsWithAudio.forEach { libraryItem -> addServerLibrary(libraryItem) }
cb(libraryItemsWithAudio)
val libraryItemsWithAudio = cachedLibraryDiscovery[libraryId]?.filter { li -> li.checkHasTracks() }
libraryItemsWithAudio?.forEach { libraryItem -> addServerLibrary(libraryItem) }
cb(libraryItemsWithAudio as List<LibraryItem>)
}

fun getLibraryRecentShelfs(libraryId: String, cb: (List<LibraryShelfType>) -> Unit) {
if (!cachedLibraryRecentShelfs.containsKey(libraryId)) {
Log.d(tag, "getLibraryRecentShelfs: No shelves $libraryId")
cb(listOf())
return
}
Expand Down Expand Up @@ -748,6 +766,25 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
}
}

fun initializeInProgressItems(cb: () -> Unit) {
Log.d(tag, "Initializing inprogress items")

loadItemsInProgressForAllLibraries { itemsInProgress ->
itemsInProgress.forEach {
val libraryItem = it.libraryItemWrapper as LibraryItem
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
serverLibraryItems.add(libraryItem)
}

if (it.episode != null) {
podcastEpisodeLibraryItemMap[it.episode.id] = LibraryItemWithEpisode(it.libraryItemWrapper, it.episode)
}
}
Log.d(tag, "Initializing inprogress items done")
cb()
}
}

fun loadAndroidAutoItems(cb: () -> Unit) {
Log.d(tag, "Load android auto items")

Expand All @@ -762,23 +799,7 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
Log.w(tag, "No libraries returned from server request")
cb()
} else {
val library = libraries[0]
Log.d(tag, "Loading categories for library ${library.name} - ${library.id} - ${library.mediaType}")

loadItemsInProgressForAllLibraries { itemsInProgress ->
itemsInProgress.forEach {
val libraryItem = it.libraryItemWrapper as LibraryItem
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
serverLibraryItems.add(libraryItem)
}

if (it.episode != null) {
podcastEpisodeLibraryItemMap[it.episode.id] = LibraryItemWithEpisode(it.libraryItemWrapper, it.episode)
}
}

cb() // Fully loaded
}
cb() // Fully loaded
}
}
} else { // Not connected to server
Expand Down
55 changes: 55 additions & 0 deletions android/app/src/main/java/com/audiobookshelf/app/media/icons.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.audiobookshelf.app.media

import android.content.ContentResolver
import android.content.Context
import android.net.Uri
import androidx.annotation.AnyRes
import com.audiobookshelf.app.R

/**
* get uri to drawable or any other resource type if u wish
* @param drawableId - drawable res id
* @return - uri
*/
fun getUriToDrawable(context: Context, @AnyRes drawableId: Int): Uri {
return Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE
+ "://" + context.resources.getResourcePackageName(drawableId)
+ '/' + context.resources.getResourceTypeName(drawableId)
+ '/' + context.resources.getResourceEntryName(drawableId))
}


/**
* get uri to drawable or any other resource type if u wish
* @param drawableId - drawable res id
* @return - uri
*/
fun getUriToAbsIconDrawable(context: Context, absIconName: String): Uri {
val drawableId = when(absIconName) {
"audiobookshelf" -> R.drawable.abs_audiobookshelf
"authors" -> R.drawable.abs_authors
"book-1" -> R.drawable.abs_book_1
"books-1" -> R.drawable.abs_books_1
"books-2" -> R.drawable.abs_books_2
"columns" -> R.drawable.abs_columns
"database" -> R.drawable.abs_database
"file-picture" -> R.drawable.abs_file_picture
"headphones" -> R.drawable.abs_headphones
"heart" -> R.drawable.abs_heart
"microphone_1" -> R.drawable.abs_microphone_1
"microphone_2" -> R.drawable.abs_microphone_2
"microphone_3" -> R.drawable.abs_microphone_3
"music" -> R.drawable.abs_music
"podcast" -> R.drawable.abs_podcast
"radio" -> R.drawable.abs_radio
"rocket" -> R.drawable.abs_rocket
"rss" -> R.drawable.abs_rss
"star" -> R.drawable.abs_star
else -> R.drawable.icon_library_folder
}
return Uri.parse(
ContentResolver.SCHEME_ANDROID_RESOURCE
+ "://" + context.resources.getResourcePackageName(drawableId)
+ '/' + context.resources.getResourceTypeName(drawableId)
+ '/' + context.resources.getResourceEntryName(drawableId))
}
Original file line number Diff line number Diff line change
@@ -1,82 +1,74 @@
package com.audiobookshelf.app.player

import android.content.ContentResolver
import android.content.Context
import android.net.Uri
import android.support.v4.media.MediaMetadataCompat
import androidx.annotation.AnyRes
import android.util.Log
import com.audiobookshelf.app.R
import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.media.getUriToDrawable

class BrowseTree(
val context: Context,
itemsInProgress: List<ItemInProgress>,
libraries: List<Library>
libraries: List<Library>,
recentsLoaded: Boolean
) {
private val mediaIdToChildren = mutableMapOf<String, MutableList<MediaMetadataCompat>>()

/**
* get uri to drawable or any other resource type if u wish
* @param drawableId - drawable res id
* @return - uri
*/
private fun getUriToDrawable(@AnyRes drawableId: Int): Uri {
return Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE
+ "://" + context.resources.getResourcePackageName(drawableId)
+ '/' + context.resources.getResourceTypeName(drawableId)
+ '/' + context.resources.getResourceEntryName(drawableId))
}

init {
val rootList = mediaIdToChildren[AUTO_BROWSE_ROOT] ?: mutableListOf()

val continueListeningMetadata = MediaMetadataCompat.Builder().apply {
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, CONTINUE_ROOT)
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Continue")
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(R.drawable.exo_icon_localaudio).toString())
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_localaudio).toString())
}.build()

val recentMetadata = MediaMetadataCompat.Builder().apply {
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, RECENTLY_ROOT)
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Recent")
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(R.drawable.md_clock_outline).toString())
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.md_clock_outline).toString())
}.build()

val downloadsMetadata = MediaMetadataCompat.Builder().apply {
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, DOWNLOADS_ROOT)
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Downloads")
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(R.drawable.exo_icon_downloaddone).toString())
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_downloaddone).toString())
}.build()

val librariesMetadata = MediaMetadataCompat.Builder().apply {
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, LIBRARIES_ROOT)
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Libraries")
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(R.drawable.icon_library_folder).toString())
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.icon_library_folder).toString())
}.build()

if (itemsInProgress.isNotEmpty()) {
rootList += continueListeningMetadata
}

if (libraries.isNotEmpty()) {
rootList += recentMetadata
if (recentsLoaded) {
rootList += recentMetadata
}
rootList += librariesMetadata

libraries.forEach { library ->
// Skip libraries without audio content
if (library.stats?.numAudioTracks == 0) return@forEach

Log.d("BrowseTree", "Library $library | ${library.icon}")
// Generate library list items for Libraries menu
val libraryMediaMetadata = library.getMediaMetadata()
val libraryMediaMetadata = library.getMediaMetadata(context)
val children = mediaIdToChildren[LIBRARIES_ROOT] ?: mutableListOf()
children += libraryMediaMetadata
mediaIdToChildren[LIBRARIES_ROOT] = children

// Generate library list items for Recent menu
val recentlyMediaMetadata = library.getMediaMetadata("recently")
val childrenRecently = mediaIdToChildren[RECENTLY_ROOT] ?: mutableListOf()
childrenRecently += recentlyMediaMetadata
mediaIdToChildren[RECENTLY_ROOT] = childrenRecently
if (recentsLoaded) {
// Generate library list items for Recent menu
val recentlyMediaMetadata = library.getMediaMetadata(context,"recently")
val childrenRecently = mediaIdToChildren[RECENTLY_ROOT] ?: mutableListOf()
childrenRecently += recentlyMediaMetadata
mediaIdToChildren[RECENTLY_ROOT] = childrenRecently
}
}
}

Expand Down
Loading

0 comments on commit 802c16c

Please sign in to comment.