Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions automotive/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ android {
applicationId = "com.chamika.dashtune"
minSdk = 28
targetSdk = 36
versionCode = 19
versionName = "1.2.3"
versionCode = 20
versionName = "1.2.4"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ class DashTuneMusicService : MediaLibraryService() {

companion object {
const val ACTION_STOP_PLAYBACK = "com.chamika.dashtune.ACTION_STOP_PLAYBACK"
const val ACTION_REFRESH_LIBRARY = "com.chamika.dashtune.ACTION_REFRESH_LIBRARY"

internal const val AUDIOBOOK_POSITION_REPORT_INTERVAL_MS = 30_000L
internal const val MILLISECONDS_TO_TICKS = 10_000L
Expand Down Expand Up @@ -356,6 +357,16 @@ class DashTuneMusicService : MediaLibraryService() {
stopSelf()
return START_NOT_STICKY
}
if (intent?.action == ACTION_REFRESH_LIBRARY) {
Log.i(LOG_TAG, "Refresh library requested")
if (::callback.isInitialized) {
callback.invalidateCache()
}
if (::mediaLibrarySession.isInitialized) {
mediaLibrarySession.notifyChildrenChanged(ROOT_ID, 4, null)
}
return START_NOT_STICKY
}
return super.onStartCommand(intent, flags, startId)
}

Expand Down Expand Up @@ -425,6 +436,9 @@ class DashTuneMusicService : MediaLibraryService() {

httpDataSourceFactory.setDefaultRequestProperties(headers)

if (::callback.isInitialized) {
callback.invalidateCache()
}
mediaLibrarySession.notifyChildrenChanged(ROOT_ID, 4, null)
cacheFavouriteTracks()
}
Expand Down
124 changes: 108 additions & 16 deletions automotive/src/main/java/com/chamika/dashtune/DashTuneSessionCallback.kt
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,21 @@ class DashTuneSessionCallback(
const val PLAYLIST_IDS_PREF = "playlistIds"
const val PLAYLIST_INDEX_PREF = "playlistIndex"
const val PLAYLIST_TRACK_POSITON_MS_PREF = "playlistTrackPositionMs"

private const val BROWSE_TIMEOUT_MS = 8_000L
}

private lateinit var repository: MediaRepository
private lateinit var resolver: MediaItemResolver
private val initLock = Any()

fun invalidateCache() {
synchronized(initLock) {
if (::repository.isInitialized) {
repository.invalidateCache()
}
}
}

override fun onConnect(
session: MediaSession,
Expand All @@ -93,14 +104,16 @@ class DashTuneSessionCallback(
}

private fun ensureTreeInitialized(artSizeHint: Int? = null) {
if (!::repository.isInitialized) {
val artSize = artSizeHint ?: 1024
Log.d(LOG_TAG, "Initializing media tree with art size: $artSize")

val itemFactory = MediaItemFactory(service, jellyfinApi, artSize)
val tree = JellyfinMediaTree(service, jellyfinApi, itemFactory)
repository = MediaRepository(mediaCacheDao, tree, itemFactory)
resolver = MediaItemResolver(repository)
synchronized(initLock) {
if (!::repository.isInitialized) {
val artSize = artSizeHint ?: 1024
Log.d(LOG_TAG, "Initializing media tree with art size: $artSize")

val itemFactory = MediaItemFactory(service, jellyfinApi, artSize)
val tree = JellyfinMediaTree(service, jellyfinApi, itemFactory)
repository = MediaRepository(mediaCacheDao, tree, itemFactory)
resolver = MediaItemResolver(repository)
}
}
}

Expand All @@ -111,6 +124,19 @@ class DashTuneSessionCallback(
): ListenableFuture<LibraryResult<MediaItem>> {
Log.i(LOG_TAG, "onGetRoot")

if (!accountManager.isAuthenticated) {
return Futures.immediateFuture(
LibraryResult.ofError(
SessionError(
SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED,
service.getString(R.string.sign_in_to_your_jellyfin_server)
),
MediaLibraryService.LibraryParams.Builder()
.setExtras(authenticationExtras()).build()
)
)
}

val artSize = params?.extras?.getInt(EXTRAS_KEY_MEDIA_ART_SIZE_PIXELS)
ensureTreeInitialized(artSize)

Expand All @@ -124,7 +150,13 @@ class DashTuneSessionCallback(
Log.e(LOG_TAG, "Failed to get library root", e)
FirebaseUtils.safeSetCustomKey("failed_operation", "get_library_root")
FirebaseUtils.safeRecordException(e)
LibraryResult.ofError(SessionError(SessionError.ERROR_UNKNOWN, ""))
LibraryResult.ofError(
SessionError(
SessionError.ERROR_UNKNOWN,
service.getString(R.string.library_unavailable)
),
retryErrorParams()
)
}
}
}
Expand All @@ -151,15 +183,26 @@ class DashTuneSessionCallback(
)
}

ensureTreeInitialized()

return SuspendToFutureAdapter.launchFuture {
try {
LibraryResult.ofItemList(repository.getChildren(parentId), params)
val children = withTimeoutOrNull(BROWSE_TIMEOUT_MS) {
repository.getChildren(parentId)
} ?: throw java.util.concurrent.TimeoutException("Browse timed out after ${BROWSE_TIMEOUT_MS}ms")
LibraryResult.ofItemList(children, params)
} catch (e: Exception) {
Log.e(LOG_TAG, "Failed to get children for $parentId", e)
FirebaseUtils.safeSetCustomKey("failed_operation", "get_children")
FirebaseUtils.safeSetCustomKey("parent_id", parentId)
FirebaseUtils.safeRecordException(e)
LibraryResult.ofError(SessionError(SessionError.ERROR_UNKNOWN, "Failed to get media items"))
LibraryResult.ofError(
SessionError(
SessionError.ERROR_UNKNOWN,
service.getString(R.string.library_unavailable)
),
retryErrorParams()
)
}
}
}
Expand All @@ -186,12 +229,37 @@ class DashTuneSessionCallback(
}
}

private fun retryExtras(): Bundle {
return Bundle().also {
it.putString(
EXTRAS_KEY_ERROR_RESOLUTION_ACTION_LABEL_COMPAT,
service.getString(R.string.retry)
)

val refreshIntent = Intent(service, DashTuneMusicService::class.java).apply {
action = DashTuneMusicService.ACTION_REFRESH_LIBRARY
}
val flags = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
val pending = PendingIntent.getService(service, 0, refreshIntent, flags)

it.putParcelable(EXTRAS_KEY_ERROR_RESOLUTION_ACTION_INTENT_COMPAT, pending)
it.putParcelable(EXTRAS_KEY_ERROR_RESOLUTION_USING_CAR_APP_LIBRARY_INTENT_COMPAT, pending)
}
}

private fun retryErrorParams(): MediaLibraryService.LibraryParams {
return MediaLibraryService.LibraryParams.Builder()
.setExtras(retryExtras())
.build()
}

override fun onGetItem(
session: MediaLibraryService.MediaLibrarySession,
browser: MediaSession.ControllerInfo,
mediaId: String,
): ListenableFuture<LibraryResult<MediaItem>> {
Log.i(LOG_TAG, "onGetItem $mediaId")
ensureTreeInitialized()
return SuspendToFutureAdapter.launchFuture {
try {
LibraryResult.ofItem(
Expand All @@ -202,7 +270,13 @@ class DashTuneSessionCallback(
Log.e(LOG_TAG, "Failed to get item $mediaId", e)
FirebaseUtils.safeSetCustomKey("failed_operation", "get_item")
FirebaseUtils.safeRecordException(e)
LibraryResult.ofError(SessionError(SessionError.ERROR_UNKNOWN, "Failed to get media item"))
LibraryResult.ofError(
SessionError(
SessionError.ERROR_UNKNOWN,
service.getString(R.string.library_unavailable)
),
retryErrorParams()
)
}
}
}
Expand Down Expand Up @@ -314,17 +388,26 @@ class DashTuneSessionCallback(
query: String,
params: MediaLibraryService.LibraryParams?
): ListenableFuture<LibraryResult<Void>> {
ensureTreeInitialized()
return SuspendToFutureAdapter.launchFuture {
try {
val results = repository.search(query).size
val results = withTimeoutOrNull(BROWSE_TIMEOUT_MS) {
repository.search(query).size
} ?: throw java.util.concurrent.TimeoutException("Search timed out after ${BROWSE_TIMEOUT_MS}ms")
session.notifySearchResultChanged(browser, query, results, params)
LibraryResult.ofVoid(params)
} catch (e: Exception) {
Log.e(LOG_TAG, "Failed to search for '$query'", e)
FirebaseUtils.safeSetCustomKey("failed_operation", "search")
FirebaseUtils.safeSetCustomKey("search_query", query)
FirebaseUtils.safeRecordException(e)
LibraryResult.ofVoid()
LibraryResult.ofError(
SessionError(
SessionError.ERROR_UNKNOWN,
service.getString(R.string.library_unavailable)
),
retryErrorParams()
)
}
}
}
Expand All @@ -337,16 +420,25 @@ class DashTuneSessionCallback(
pageSize: Int,
params: MediaLibraryService.LibraryParams?
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
ensureTreeInitialized()
return SuspendToFutureAdapter.launchFuture {
try {
val results = repository.search(query)
val results = withTimeoutOrNull(BROWSE_TIMEOUT_MS) {
repository.search(query)
} ?: throw java.util.concurrent.TimeoutException("Search timed out after ${BROWSE_TIMEOUT_MS}ms")
LibraryResult.ofItemList(results, params)
} catch (e: Exception) {
Log.e(LOG_TAG, "Failed to get search results for '$query'", e)
FirebaseUtils.safeSetCustomKey("failed_operation", "get_search_result")
FirebaseUtils.safeSetCustomKey("search_query", query)
FirebaseUtils.safeRecordException(e)
LibraryResult.ofItemList(emptyList(), params)
LibraryResult.ofError(
SessionError(
SessionError.ERROR_UNKNOWN,
service.getString(R.string.library_unavailable)
),
retryErrorParams()
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ class MediaRepository(
return tree.search(query)
}

fun invalidateCache() {
tree.invalidateCache()
}

suspend fun sync(): Boolean = syncMutex.withLock {
val sectionIds = tree.getActiveCategoryIds()
val allEntities = mutableListOf<CachedMediaItemEntity>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ class JellyfinMediaTree(
.maximumSize(1000)
.build()

fun invalidateCache() {
mediaItems.invalidateAll()
}

fun getActiveCategoryIds(): List<String> {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val defaults = setOf("latest", "favourites", "books", "playlists")
Expand Down
2 changes: 2 additions & 0 deletions automotive/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,6 @@
<string name="sync_library_never">Never synced</string>
<string name="sync_library_last_synced">Last synced: %s</string>
<string name="sync_failed">Sync failed</string>
<string name="retry">Retry</string>
<string name="library_unavailable">Library unavailable. Check your connection and retry.</string>
</resources>
Loading