From 0fb6441b2c8d633f851972222a37f4dce7033225 Mon Sep 17 00:00:00 2001 From: Chamika Date: Sun, 24 May 2026 23:58:03 +0100 Subject: [PATCH] Release v1.2.4(20) Add refresh button on library error Full release notes: - Add refresh button on library error --- automotive/build.gradle.kts | 4 +- .../chamika/dashtune/DashTuneMusicService.kt | 14 ++ .../dashtune/DashTuneSessionCallback.kt | 124 +++++++++++++++--- .../chamika/dashtune/data/MediaRepository.kt | 4 + .../dashtune/media/JellyfinMediaTree.kt | 4 + automotive/src/main/res/values/strings.xml | 2 + 6 files changed, 134 insertions(+), 18 deletions(-) diff --git a/automotive/build.gradle.kts b/automotive/build.gradle.kts index 4cf853c..e602bbf 100644 --- a/automotive/build.gradle.kts +++ b/automotive/build.gradle.kts @@ -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" } diff --git a/automotive/src/main/java/com/chamika/dashtune/DashTuneMusicService.kt b/automotive/src/main/java/com/chamika/dashtune/DashTuneMusicService.kt index 0ccd726..c2f70cf 100644 --- a/automotive/src/main/java/com/chamika/dashtune/DashTuneMusicService.kt +++ b/automotive/src/main/java/com/chamika/dashtune/DashTuneMusicService.kt @@ -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 @@ -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) } @@ -425,6 +436,9 @@ class DashTuneMusicService : MediaLibraryService() { httpDataSourceFactory.setDefaultRequestProperties(headers) + if (::callback.isInitialized) { + callback.invalidateCache() + } mediaLibrarySession.notifyChildrenChanged(ROOT_ID, 4, null) cacheFavouriteTracks() } diff --git a/automotive/src/main/java/com/chamika/dashtune/DashTuneSessionCallback.kt b/automotive/src/main/java/com/chamika/dashtune/DashTuneSessionCallback.kt index 98f446f..bf19969 100644 --- a/automotive/src/main/java/com/chamika/dashtune/DashTuneSessionCallback.kt +++ b/automotive/src/main/java/com/chamika/dashtune/DashTuneSessionCallback.kt @@ -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, @@ -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) + } } } @@ -111,6 +124,19 @@ class DashTuneSessionCallback( ): ListenableFuture> { 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) @@ -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() + ) } } } @@ -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() + ) } } } @@ -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> { Log.i(LOG_TAG, "onGetItem $mediaId") + ensureTreeInitialized() return SuspendToFutureAdapter.launchFuture { try { LibraryResult.ofItem( @@ -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() + ) } } } @@ -314,9 +388,12 @@ class DashTuneSessionCallback( query: String, params: MediaLibraryService.LibraryParams? ): ListenableFuture> { + 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) { @@ -324,7 +401,13 @@ class DashTuneSessionCallback( 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() + ) } } } @@ -337,16 +420,25 @@ class DashTuneSessionCallback( pageSize: Int, params: MediaLibraryService.LibraryParams? ): ListenableFuture>> { + 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() + ) } } } diff --git a/automotive/src/main/java/com/chamika/dashtune/data/MediaRepository.kt b/automotive/src/main/java/com/chamika/dashtune/data/MediaRepository.kt index dd4d653..ae17724 100644 --- a/automotive/src/main/java/com/chamika/dashtune/data/MediaRepository.kt +++ b/automotive/src/main/java/com/chamika/dashtune/data/MediaRepository.kt @@ -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() diff --git a/automotive/src/main/java/com/chamika/dashtune/media/JellyfinMediaTree.kt b/automotive/src/main/java/com/chamika/dashtune/media/JellyfinMediaTree.kt index 214013e..b8a23c3 100644 --- a/automotive/src/main/java/com/chamika/dashtune/media/JellyfinMediaTree.kt +++ b/automotive/src/main/java/com/chamika/dashtune/media/JellyfinMediaTree.kt @@ -45,6 +45,10 @@ class JellyfinMediaTree( .maximumSize(1000) .build() + fun invalidateCache() { + mediaItems.invalidateAll() + } + fun getActiveCategoryIds(): List { val prefs = PreferenceManager.getDefaultSharedPreferences(context) val defaults = setOf("latest", "favourites", "books", "playlists") diff --git a/automotive/src/main/res/values/strings.xml b/automotive/src/main/res/values/strings.xml index 7fbe5fb..5e8f301 100644 --- a/automotive/src/main/res/values/strings.xml +++ b/automotive/src/main/res/values/strings.xml @@ -40,4 +40,6 @@ Never synced Last synced: %s Sync failed + Retry + Library unavailable. Check your connection and retry.