From f3dd7808acbf19c30c6d647fd02f9b3916be94b2 Mon Sep 17 00:00:00 2001 From: birdbird <6892457-tzugen@users.noreply.gitlab.com> Date: Thu, 1 Oct 2026 18:46:08 +0000 Subject: [PATCH] Surface jukebox error message before falling back to local playback --- .../ultrasonic/service/PlaybackService.kt | 67 ++++++++++++++++--- ultrasonic/src/main/res/values/strings.xml | 2 + 2 files changed, 61 insertions(+), 8 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackService.kt index b4b0480a..3f6b961d 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackService.kt @@ -20,6 +20,7 @@ import android.os.Looper import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.app.TaskStackBuilder +import androidx.core.text.HtmlCompat import androidx.media3.common.AudioAttributes import androidx.media3.common.C import androidx.media3.common.C.USAGE_MEDIA @@ -556,7 +557,62 @@ class PlaybackService : * Connectivity failures additionally trigger a background reachability probe; only a * failed probe - never the playback error itself - switches the library to automatic * offline mode. + * Surfaces the reason why Jukebox playback failed before [updateBackend] falls back to + * local playback. Jukebox errors carry a string resource as their error code (see + * [JukeboxMediaPlayer.onError]); those messages are shown in an error notification. + * Any other error is only logged. */ + private fun notifyJukeboxError(error: PlaybackException) { + val messageRes = when (error.errorCode) { + R.string.download_jukebox_not_authorized, + R.string.download_jukebox_server_too_old, + R.string.download_jukebox_offline -> error.errorCode + + else -> return + } + + val notificationManagerCompat = NotificationManagerCompat.from(this@PlaybackService) + NotificationUtil.ensureNotificationChannel( + id = NOTIFICATION_CHANNEL_ID, + name = NOTIFICATION_CHANNEL_NAME, + notificationManager = notificationManagerCompat + ) + + // The Jukebox messages contain inline markup (e.g. ), render it for the notification + val message = HtmlCompat.fromHtml( + getString(messageRes), + HtmlCompat.FROM_HTML_MODE_LEGACY + ) + val fallback = getString(R.string.jukebox_fallback_text) + + val builder = + NotificationCompat.Builder(this@PlaybackService, NOTIFICATION_CHANNEL_ID) + .setContentIntent(createContentPendingIntent()) + .setSmallIcon(R.drawable.media3_notification_small_icon) + .setContentTitle(getString(R.string.jukebox_error_title)) + .setStyle( + NotificationCompat.BigTextStyle().bigText( + "$message\n\n$fallback" + ) + ) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setAutoCancel(true) + + NotificationUtil.postNotificationIfPermitted( + notificationManagerCompat, + JUKEBOX_ERROR_NOTIFICATION_ID, + builder.build() + ) + } + + private fun createContentPendingIntent(): PendingIntent = + TaskStackBuilder.create(this@PlaybackService).run { + addNextIntent(Intent(this@PlaybackService, NavigationActivity::class.java)) + + val immutableFlag = FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT + getPendingIntent(0, immutableFlag)!! + } + private fun recoverFromPlaybackError(error: PlaybackException) { val connectivity = error.hasTransientConnectivityFailure() if (!connectivity && !error.hasOfflineCause()) return @@ -639,6 +695,7 @@ class PlaybackService : Timber.w(error, "Playback error") if (actualBackend == PlaybackBackend.JUKEBOX) { mainHandler.post { + notifyJukeboxError(error) updateBackend(PlaybackBackend.LOCAL, retainDownloadsOnly = false) } return @@ -822,16 +879,9 @@ class PlaybackService : name = NOTIFICATION_CHANNEL_NAME, notificationManager = notificationManagerCompat ) - val pendingIntent = - TaskStackBuilder.create(this@PlaybackService).run { - addNextIntent(Intent(this@PlaybackService, NavigationActivity::class.java)) - - val immutableFlag = FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT - getPendingIntent(0, immutableFlag) - } val builder = NotificationCompat.Builder(this@PlaybackService, NOTIFICATION_CHANNEL_ID) - .setContentIntent(pendingIntent) + .setContentIntent(createContentPendingIntent()) .setSmallIcon(R.drawable.media3_notification_small_icon) .setContentTitle(getString(R.string.foreground_exception_title)) .setStyle( @@ -874,5 +924,6 @@ class PlaybackService : const val COMMAND_ARGUMENT_FROM_START = "fromStart" const val COMMAND_ARGUMENT_BACKEND = "backend" private const val NOTIFICATION_ID = 3009 + private const val JUKEBOX_ERROR_NOTIFICATION_ID = 3010 } } diff --git a/ultrasonic/src/main/res/values/strings.xml b/ultrasonic/src/main/res/values/strings.xml index a1d74267..5085274b 100644 --- a/ultrasonic/src/main/res/values/strings.xml +++ b/ultrasonic/src/main/res/values/strings.xml @@ -65,6 +65,8 @@ Remote control is not available in offline mode. Turned on remote control. Music is played on server. Remote control is not supported. Please upgrade your Subsonic server. + Jukebox playback failed + Switching to local playback on this device. Equalizer Jukebox Off Jukebox On