From 529fd1478259d2bea412cdc47e76186ef9f94c33 Mon Sep 17 00:00:00 2001 From: Chamika Date: Mon, 25 May 2026 00:23:04 +0100 Subject: [PATCH 1/3] Fix misleading "Check Google Play" dialog when offline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The AAOS generic error dialog appeared over the Now Playing screen whenever ExoPlayer entered an error state with no handler. With Firebase and Google Play Services already removed, the dialog text is just the system's hardcoded template — the trigger was an unhandled PlaybackException, not a real GMS check. - Override Player.Listener.onPlayerError in DashTuneMusicService to recover gracefully: skip to the next item if available, otherwise stop and clear, so the session stops publishing an error state. - Filter onPlaybackResumption to drop saved tracks that aren't cached when offline, avoiding a known-broken queue on cold offline launch. - Harden AlbumArtContentProvider.openFile against network failures: catch IOException, throw the documented FileNotFoundException when no file ends up on disk, and add 10s OkHttp timeouts. Co-Authored-By: Claude Opus 4.7 --- .../dashtune/AlbumArtContentProvider.kt | 54 ++++++++++++------- .../chamika/dashtune/DashTuneMusicService.kt | 35 ++++++++++++ .../dashtune/DashTuneSessionCallback.kt | 30 ++++++++--- 3 files changed, 91 insertions(+), 28 deletions(-) diff --git a/automotive/src/main/java/com/chamika/dashtune/AlbumArtContentProvider.kt b/automotive/src/main/java/com/chamika/dashtune/AlbumArtContentProvider.kt index 74be0a8..cf3c6d2 100644 --- a/automotive/src/main/java/com/chamika/dashtune/AlbumArtContentProvider.kt +++ b/automotive/src/main/java/com/chamika/dashtune/AlbumArtContentProvider.kt @@ -14,12 +14,16 @@ import okio.buffer import okio.sink import java.io.File import java.io.FileNotFoundException +import java.io.IOException import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit class AlbumArtContentProvider : ContentProvider() { - private val client = OkHttpClient() + private val client = OkHttpClient.Builder() + .connectTimeout(10, TimeUnit.SECONDS) + .readTimeout(10, TimeUnit.SECONDS) + .build() companion object { private val uriMap = mutableMapOf() @@ -86,28 +90,38 @@ class AlbumArtContentProvider : ContentProvider() { .build() Log.d(LOG_TAG, "Downloading $remoteUri ...") - client.newCall(request).execute().use { - if (it.code == 200) { - Log.d(LOG_TAG, "Downloaded $remoteUri") - val source = it.body.source() - source.request(Long.MAX_VALUE) - - val sink = tmpFile.sink().buffer() - sink.writeAll(source) - sink.flush() - sink.close() - - tmpFile.renameTo(file) - } else { - Log.w(LOG_TAG, "Failed to download $remoteUri: \n ${it.code} - ${it.body}") - FirebaseUtils.safeSetCustomKey("album_art_path", remoteUri.path ?: "unknown") - FirebaseUtils.safeRecordException(Exception("Album art download failed: HTTP ${it.code}")) + try { + client.newCall(request).execute().use { + if (it.code == 200) { + Log.d(LOG_TAG, "Downloaded $remoteUri") + val source = it.body.source() + source.request(Long.MAX_VALUE) + + val sink = tmpFile.sink().buffer() + sink.writeAll(source) + sink.flush() + sink.close() + + tmpFile.renameTo(file) + } else { + Log.w(LOG_TAG, "Failed to download $remoteUri: \n ${it.code} - ${it.body}") + FirebaseUtils.safeSetCustomKey("album_art_path", remoteUri.path ?: "unknown") + FirebaseUtils.safeRecordException(Exception("Album art download failed: HTTP ${it.code}")) + } + } + } catch (e: IOException) { + Log.w(LOG_TAG, "Network error downloading $remoteUri", e) + } finally { + tmpFile.delete() + synchronized(inProgress) { + inProgress[remoteUri]?.countDown() + inProgress.remove(remoteUri) } - - inProgress.get(remoteUri)?.countDown() - inProgress.remove(remoteUri) } + if (!file.exists()) { + throw FileNotFoundException(uri.path) + } return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY) } diff --git a/automotive/src/main/java/com/chamika/dashtune/DashTuneMusicService.kt b/automotive/src/main/java/com/chamika/dashtune/DashTuneMusicService.kt index c2f70cf..72f2219 100644 --- a/automotive/src/main/java/com/chamika/dashtune/DashTuneMusicService.kt +++ b/automotive/src/main/java/com/chamika/dashtune/DashTuneMusicService.kt @@ -193,6 +193,23 @@ class DashTuneMusicService : MediaLibraryService() { } playerListener = object : Player.Listener { + override fun onPlayerError(error: androidx.media3.common.PlaybackException) { + val p = mediaLibrarySession.player + val failedId = p.currentMediaItem?.mediaId + Log.e(LOG_TAG, "Player error on $failedId: ${error.errorCodeName}", error) + FirebaseUtils.safeSetCustomKey("player_error_code", error.errorCodeName) + FirebaseUtils.safeSetCustomKey("failed_track_id", failedId ?: "") + FirebaseUtils.safeRecordException(error) + + if (p.hasNextMediaItem()) { + p.seekToNextMediaItem() + p.prepare() + } else { + p.stop() + p.clearMediaItems() + } + } + override fun onEvents(player: Player, events: Player.Events) { if (events.contains(Player.EVENT_MEDIA_ITEM_TRANSITION)) { // Save audiobook position for the OLD track before transition @@ -419,6 +436,24 @@ class DashTuneMusicService : MediaLibraryService() { super.onUpdateNotification(session, startInForegroundRequired) } + fun isTrackCachedOrOnline(mediaId: String): Boolean { + if (hasInternet()) return true + return try { + val state = downloadManager.downloadIndex.getDownload(mediaId)?.state + state == Download.STATE_COMPLETED + } catch (e: Exception) { + Log.w(LOG_TAG, "Failed to check cache for $mediaId", e) + false + } + } + + private fun hasInternet(): Boolean { + val net = connectivityManager.activeNetwork ?: return false + val caps = connectivityManager.getNetworkCapabilities(net) ?: return false + return caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && + caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + } + fun onLogin() { FirebaseUtils.safeLog("Login applied to service") jellyfinApi.update( diff --git a/automotive/src/main/java/com/chamika/dashtune/DashTuneSessionCallback.kt b/automotive/src/main/java/com/chamika/dashtune/DashTuneSessionCallback.kt index bf19969..a63a8b9 100644 --- a/automotive/src/main/java/com/chamika/dashtune/DashTuneSessionCallback.kt +++ b/automotive/src/main/java/com/chamika/dashtune/DashTuneSessionCallback.kt @@ -455,21 +455,35 @@ class DashTuneSessionCallback( try { val prefs = PreferenceManager.getDefaultSharedPreferences(service) - val mediaItemsToRestore = prefs + val savedIds = prefs .getString(PLAYLIST_IDS_PREF, "") ?.split(",") ?.filter { it.isNotEmpty() } - ?.map { async { repository.getItem(it) } } - ?.awaitAll() ?: listOf() + ?: emptyList() + + val playableIds = savedIds.filter { service.isTrackCachedOrOnline(it) } + if (playableIds.size < savedIds.size) { + Log.i(LOG_TAG, "Skipping ${savedIds.size - playableIds.size} unplayable tracks (offline + uncached)") + } + + val mediaItemsToRestore = playableIds + .map { async { repository.getItem(it) } } + .awaitAll() Log.d(LOG_TAG, "Resuming playback with $mediaItemsToRestore") FirebaseUtils.safeLog("Restoring playback: index=${prefs.getInt(PLAYLIST_INDEX_PREF, 0)}, trackCount=${mediaItemsToRestore.size}") - MediaSession.MediaItemsWithStartPosition( - mediaItemsToRestore, - prefs.getInt(PLAYLIST_INDEX_PREF, 0), - prefs.getLong(PLAYLIST_TRACK_POSITON_MS_PREF, 0), - ) + if (mediaItemsToRestore.isEmpty()) { + MediaSession.MediaItemsWithStartPosition(emptyList(), 0, 0L) + } else { + val savedIndex = prefs.getInt(PLAYLIST_INDEX_PREF, 0) + .coerceIn(0, mediaItemsToRestore.lastIndex) + MediaSession.MediaItemsWithStartPosition( + mediaItemsToRestore, + savedIndex, + prefs.getLong(PLAYLIST_TRACK_POSITON_MS_PREF, 0), + ) + } } catch (e: Exception) { Log.e(LOG_TAG, "Failed to resume playback", e) FirebaseUtils.safeRecordException(e) From 846b2552357da4de696f4ec92320c7dbf6dd779c Mon Sep 17 00:00:00 2001 From: Chamika Date: Mon, 25 May 2026 00:46:18 +0100 Subject: [PATCH 2/3] Fix blank Media Center screen for signed-out users MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Returning LibraryResult.ofError from onGetLibraryRoot makes the Media3 to legacy MediaBrowserService bridge deliver a null root to AAOS Media Center, which responds with onConnectFailed and a blank screen — the sign-in PendingIntent in the error's extras never fires. Always return a valid root with the auth extras on the params instead; the existing auth check in onGetChildren still launches SignInActivity via the same PendingIntent. Co-Authored-By: Claude Opus 4.7 --- .../dashtune/DashTuneSessionCallback.kt | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/automotive/src/main/java/com/chamika/dashtune/DashTuneSessionCallback.kt b/automotive/src/main/java/com/chamika/dashtune/DashTuneSessionCallback.kt index a63a8b9..40b28e4 100644 --- a/automotive/src/main/java/com/chamika/dashtune/DashTuneSessionCallback.kt +++ b/automotive/src/main/java/com/chamika/dashtune/DashTuneSessionCallback.kt @@ -124,28 +124,25 @@ 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) + // Always return a valid root MediaItem. Returning LibraryResult.ofError here + // causes the Media3 -> legacy MediaBrowserService bridge to deliver a null root + // to the AAOS Media Center, which then fails with onConnectFailed and shows + // a blank screen instead of the sign-in button. The unauthenticated case is + // surfaced via the auth extras on the root params and the auth check in + // onGetChildren. return SuspendToFutureAdapter.launchFuture { try { - LibraryResult.ofItem( - repository.getItem(ROOT_ID), + val rootItem = repository.getItem(ROOT_ID) + val outParams = if (!accountManager.isAuthenticated) { + MediaLibraryService.LibraryParams.Builder() + .setExtras(authenticationExtras()).build() + } else { params - ) + } + LibraryResult.ofItem(rootItem, outParams) } catch (e: Exception) { Log.e(LOG_TAG, "Failed to get library root", e) FirebaseUtils.safeSetCustomKey("failed_operation", "get_library_root") From 81c5ffd98cbf76d244274712f737d97c55bc4a82 Mon Sep 17 00:00:00 2001 From: Chamika Date: Mon, 25 May 2026 00:46:34 +0100 Subject: [PATCH 3/3] Release v1.2.5(21) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix blank screen on fresh install plus offline error dialog and auto-resume crash Full release notes: - Fix blank Media Center screen when opening the app while signed out — the sign-in prompt now appears correctly again - Fix the "Something went wrong / Check Google Play" dialog that appeared on the Now Playing screen when the car had no internet - Skip auto-resuming previously played tracks that aren't cached on the device when offline, instead of getting stuck on "Loading content…" - Album art now falls back to a placeholder when offline instead of failing --- automotive/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/automotive/build.gradle.kts b/automotive/build.gradle.kts index e602bbf..04242d4 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 = 20 - versionName = "1.2.4" + versionCode = 21 + versionName = "1.2.5" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" }