From 2a599487cb55faca266765a56a32ca55d4492df5 Mon Sep 17 00:00:00 2001 From: Tim Malseed Date: Thu, 15 Jan 2026 21:36:02 +1100 Subject: [PATCH 1/9] Potential crash fix for foreground service issue on Samsung devices --- .../playback/PlaybackNotificationManager.kt | 4 ++++ .../playback/PlaybackService.kt | 23 ++++++++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/android/playback/src/main/java/com/simplecityapps/playback/PlaybackNotificationManager.kt b/android/playback/src/main/java/com/simplecityapps/playback/PlaybackNotificationManager.kt index a273a34f8..68f5ce4a5 100644 --- a/android/playback/src/main/java/com/simplecityapps/playback/PlaybackNotificationManager.kt +++ b/android/playback/src/main/java/com/simplecityapps/playback/PlaybackNotificationManager.kt @@ -183,6 +183,10 @@ constructor( notificationManager.cancel(NOTIFICATION_ID) } + fun notify(notification: Notification) { + notificationManager.notify(NOTIFICATION_ID, notification) + } + private val playbackAction: NotificationCompat.Action get() { val intent = diff --git a/android/playback/src/main/java/com/simplecityapps/playback/PlaybackService.kt b/android/playback/src/main/java/com/simplecityapps/playback/PlaybackService.kt index d168284f6..8b258e86a 100644 --- a/android/playback/src/main/java/com/simplecityapps/playback/PlaybackService.kt +++ b/android/playback/src/main/java/com/simplecityapps/playback/PlaybackService.kt @@ -1,5 +1,7 @@ package com.simplecityapps.playback +import android.app.ForegroundServiceStartNotAllowedException +import android.app.Notification import android.app.SearchManager import android.app.Service import android.content.Intent @@ -123,16 +125,16 @@ class PlaybackService : This also gives S2 time to respond to pending commands. For example, if the command is 'loadFromSearch', we don't want to stop the service and allow the process to be killed while that we're in the middle of executing that command. */ - startForeground(PlaybackNotificationManager.NOTIFICATION_ID, notificationManager.displayQueueEmptyNotification()) + startForegroundSafely(notificationManager.displayQueueEmptyNotification()) postDelayedShutdown(10000) } else { Timber.v("startForeground() called. Showing notification: Playback") - startForeground(PlaybackNotificationManager.NOTIFICATION_ID, notificationManager.displayPlaybackNotification()) + startForegroundSafely(notificationManager.displayPlaybackNotification()) } processCommand(intent) } else { Timber.v("startForeground() called. Showing notification: Loading") - startForeground(PlaybackNotificationManager.NOTIFICATION_ID, notificationManager.displayLoadingNotification()) + startForegroundSafely(notificationManager.displayLoadingNotification()) pendingStartCommands.add(intent) } } @@ -178,6 +180,21 @@ class PlaybackService : // Private + private fun startForegroundSafely(notification: Notification) { + try { + startForeground(PlaybackNotificationManager.NOTIFICATION_ID, notification) + } catch (e: IllegalStateException) { + // ForegroundServiceStartNotAllowedException (API 31+) extends IllegalStateException + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && e is ForegroundServiceStartNotAllowedException) { + Timber.w(e, "Unable to start foreground service - likely started from background context (e.g., media button broadcast)") + // Still display the notification even if we can't start foreground + notificationManager.notify(notification) + } else { + throw e + } + } + } + private fun processCommand(intent: Intent) { Timber.v("processCommand()") MediaButtonReceiver.handleIntent(mediaSessionManager.mediaSession, intent) From 9260eb3529413c91b3d0aa04ec5567d0736d7a6d Mon Sep 17 00:00:00 2001 From: Tim Malseed Date: Thu, 15 Jan 2026 21:59:25 +1100 Subject: [PATCH 2/9] Fixed a crash when viewing licenses dialog --- android/app/build.gradle.kts | 4 ++-- gradle/libs.versions.toml | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 10da28382..20dcc6fc0 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -4,7 +4,7 @@ plugins { id("com.android.application") id("kotlin-android") id("androidx.navigation.safeargs.kotlin") - id("com.mikepenz.aboutlibraries.plugin") + id("com.mikepenz.aboutlibraries.plugin.android") id("kotlin-parcelize") id("dagger.hilt.android.plugin") id("com.google.firebase.crashlytics") @@ -239,7 +239,7 @@ android { } // About Libraries - implementation(libs.mikepenz.aboutlibrariesCore) + implementation(libs.mikepenz.aboutlibraries) // Billing implementation(libs.billingclient.billingKtx) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 051555a15..977ef8d08 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -152,6 +152,7 @@ kotlinx-coroutinesPlayServices = { module = "org.jetbrains.kotlinx:kotlinx-corou kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" } leakcanary-android = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanary-android" } mikepenz-aboutlibrariesCore = { module = "com.mikepenz:aboutlibraries-core", version.ref = "aboutlibraries" } +mikepenz-aboutlibraries = { module = "com.mikepenz:aboutlibraries", version.ref = "aboutlibraries" } moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" } moshi-adapters = { module = "com.squareup.moshi:moshi-adapters", version.ref = "moshi-adapters" } moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshi-kotlin" } @@ -175,6 +176,7 @@ vdurmont-semver4j = { module = "com.vdurmont:semver4j", version.ref = "semver4j" [plugins] aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutlibraries" } +aboutlibraries-android = { id = "com.mikepenz.aboutlibraries.plugin.android", version.ref = "aboutlibraries" } android-application = { id = "com.android.application", version.ref = "agp" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } From 75657d539086eaa56c45a382563343b07bb36eff Mon Sep 17 00:00:00 2001 From: Tim Malseed Date: Thu, 15 Jan 2026 22:09:28 +1100 Subject: [PATCH 3/9] Potential fix for crash on devices that don't support Cast --- .../shuttle/ui/screens/playback/PlaybackFragment.kt | 10 +++++++++- .../playback/chromecast/CastSessionManager.kt | 13 +++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/android/app/src/main/java/com/simplecityapps/shuttle/ui/screens/playback/PlaybackFragment.kt b/android/app/src/main/java/com/simplecityapps/shuttle/ui/screens/playback/PlaybackFragment.kt index ff0019061..6a2b9d799 100644 --- a/android/app/src/main/java/com/simplecityapps/shuttle/ui/screens/playback/PlaybackFragment.kt +++ b/android/app/src/main/java/com/simplecityapps/shuttle/ui/screens/playback/PlaybackFragment.kt @@ -20,6 +20,7 @@ import com.google.android.gms.cast.framework.CastButtonFactory import com.simplecityapps.adapter.RecyclerAdapter import com.simplecityapps.adapter.RecyclerListener import com.simplecityapps.playback.PlaybackState +import com.simplecityapps.playback.chromecast.CastSessionManager import com.simplecityapps.playback.queue.QueueItem import com.simplecityapps.playback.queue.QueueManager import com.simplecityapps.shuttle.R @@ -64,6 +65,9 @@ class PlaybackFragment : @Inject lateinit var queueManager: QueueManager + @Inject + lateinit var castSessionManager: CastSessionManager + private var recyclerView: RecyclerView by autoCleared() private var adapter: RecyclerAdapter by autoCleared() @@ -224,7 +228,11 @@ class PlaybackFragment : presenter.setFavorite(favoriteButton.isChecked) } - CastButtonFactory.setUpMediaRouteButton(requireContext(), toolbar.menu, R.id.media_route_menu_item) + if (castSessionManager.isAvailable) { + CastButtonFactory.setUpMediaRouteButton(requireContext(), toolbar.menu, R.id.media_route_menu_item) + } else { + toolbar.menu.findItem(R.id.media_route_menu_item)?.isVisible = false + } savedInstanceState?.getParcelable(QueueFragment.ARG_RECYCLER_STATE)?.let { recyclerViewState = it } diff --git a/android/playback/src/main/java/com/simplecityapps/playback/chromecast/CastSessionManager.kt b/android/playback/src/main/java/com/simplecityapps/playback/chromecast/CastSessionManager.kt index fbdd60c2d..e8348c5da 100644 --- a/android/playback/src/main/java/com/simplecityapps/playback/chromecast/CastSessionManager.kt +++ b/android/playback/src/main/java/com/simplecityapps/playback/chromecast/CastSessionManager.kt @@ -19,9 +19,18 @@ constructor( private val exoPlayerPlayback: ExoPlayerPlayback, private val mediaInfoProvider: MediaInfoProvider ) : SessionManagerListener { + var isAvailable: Boolean = false + private set + init { - val sessionManager = CastContext.getSharedInstance(applicationContext).sessionManager - sessionManager.addSessionManagerListener(this, CastSession::class.java) + try { + val sessionManager = CastContext.getSharedInstance(applicationContext).sessionManager + sessionManager.addSessionManagerListener(this, CastSession::class.java) + isAvailable = true + } catch (e: Exception) { + // Cast framework unavailable on this device (e.g., no Google Play Services) + Timber.w(e, "Failed to initialize Cast framework - Chromecast will be unavailable") + } } override fun onSessionStarting(castSession: CastSession) { From 712ecca06b8e579224094b2a3b983172d01e9b3e Mon Sep 17 00:00:00 2001 From: Tim Malseed Date: Thu, 15 Jan 2026 22:28:50 +1100 Subject: [PATCH 4/9] More possible foreground service crash fixes --- .../appinitializers/PlaybackInitializer.kt | 12 +++++++++- .../simplecityapps/shuttle/ui/MainActivity.kt | 24 +++++++++++++------ .../shuttle/ui/ShortcutHandlerActivity.kt | 18 ++++++++++---- 3 files changed, 42 insertions(+), 12 deletions(-) diff --git a/android/app/src/main/java/com/simplecityapps/shuttle/appinitializers/PlaybackInitializer.kt b/android/app/src/main/java/com/simplecityapps/shuttle/appinitializers/PlaybackInitializer.kt index a4b1afd5f..15b07baf4 100644 --- a/android/app/src/main/java/com/simplecityapps/shuttle/appinitializers/PlaybackInitializer.kt +++ b/android/app/src/main/java/com/simplecityapps/shuttle/appinitializers/PlaybackInitializer.kt @@ -2,8 +2,10 @@ package com.simplecityapps.shuttle.appinitializers import android.annotation.SuppressLint import android.app.Application +import android.app.ForegroundServiceStartNotAllowedException import android.content.Context import android.content.Intent +import android.os.Build import androidx.core.content.ContextCompat import com.simplecityapps.mediaprovider.repository.songs.SongRepository import com.simplecityapps.playback.NoiseManager @@ -158,7 +160,15 @@ constructor( override fun onPlaybackStateChanged(playbackState: PlaybackState) { when (playbackState) { is PlaybackState.Playing -> { - ContextCompat.startForegroundService(context, Intent(context, PlaybackService::class.java)) + try { + ContextCompat.startForegroundService(context, Intent(context, PlaybackService::class.java)) + } catch (e: IllegalStateException) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && e is ForegroundServiceStartNotAllowedException) { + Timber.w(e, "Cannot start foreground service from background - likely audio focus regained while app in background") + } else { + throw e + } + } } is PlaybackState.Paused -> { playbackPreferenceManager.playbackPosition = playbackManager.getProgress() diff --git a/android/app/src/main/java/com/simplecityapps/shuttle/ui/MainActivity.kt b/android/app/src/main/java/com/simplecityapps/shuttle/ui/MainActivity.kt index d5b8e87b3..a63650811 100644 --- a/android/app/src/main/java/com/simplecityapps/shuttle/ui/MainActivity.kt +++ b/android/app/src/main/java/com/simplecityapps/shuttle/ui/MainActivity.kt @@ -1,11 +1,13 @@ package com.simplecityapps.shuttle.ui import android.Manifest +import android.app.ForegroundServiceStartNotAllowedException import android.content.Intent import android.content.pm.PackageManager import android.os.Build import android.os.Bundle import android.provider.MediaStore +import timber.log.Timber import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import androidx.navigation.fragment.NavHostFragment @@ -104,15 +106,23 @@ class MainActivity : AppCompatActivity() { private fun handleSearchQuery(intent: Intent?) { if (intent?.action == MediaStore.INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH) { - ContextCompat.startForegroundService( - this, - Intent(this, PlaybackService::class.java).apply { - action = PlaybackService.ACTION_SEARCH - intent.extras?.let { extras -> - putExtras(extras) + try { + ContextCompat.startForegroundService( + this, + Intent(this, PlaybackService::class.java).apply { + action = PlaybackService.ACTION_SEARCH + intent.extras?.let { extras -> + putExtras(extras) + } } + ) + } catch (e: IllegalStateException) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && e is ForegroundServiceStartNotAllowedException) { + Timber.w(e, "Cannot start foreground service from search query - app may be in restricted state") + } else { + throw e } - ) + } } } } diff --git a/android/app/src/main/java/com/simplecityapps/shuttle/ui/ShortcutHandlerActivity.kt b/android/app/src/main/java/com/simplecityapps/shuttle/ui/ShortcutHandlerActivity.kt index 0c55dabfb..87d63ae7d 100644 --- a/android/app/src/main/java/com/simplecityapps/shuttle/ui/ShortcutHandlerActivity.kt +++ b/android/app/src/main/java/com/simplecityapps/shuttle/ui/ShortcutHandlerActivity.kt @@ -1,11 +1,13 @@ package com.simplecityapps.shuttle.ui +import android.app.ForegroundServiceStartNotAllowedException import android.content.Intent import android.os.Build import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import com.simplecityapps.playback.PlaybackService import dagger.hilt.android.AndroidEntryPoint +import timber.log.Timber @AndroidEntryPoint class ShortcutHandlerActivity : AppCompatActivity() { @@ -19,10 +21,18 @@ class ShortcutHandlerActivity : AppCompatActivity() { action = PlaybackService.ACTION_TOGGLE_PLAYBACK } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - startForegroundService(serviceIntent) - } else { - startService(serviceIntent) + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundService(serviceIntent) + } else { + startService(serviceIntent) + } + } catch (e: IllegalStateException) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && e is ForegroundServiceStartNotAllowedException) { + Timber.w(e, "Cannot start foreground service from shortcut - app may be in restricted state") + } else { + throw e + } } } } From 0ff6c969b5ccb6d6b765d2515c4ee7eaea2a1339 Mon Sep 17 00:00:00 2001 From: Tim Malseed Date: Thu, 15 Jan 2026 22:36:19 +1100 Subject: [PATCH 5/9] Potential fix for NanoHTTPD issues --- .../playback/chromecast/CastSessionManager.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/android/playback/src/main/java/com/simplecityapps/playback/chromecast/CastSessionManager.kt b/android/playback/src/main/java/com/simplecityapps/playback/chromecast/CastSessionManager.kt index e8348c5da..ecafabec4 100644 --- a/android/playback/src/main/java/com/simplecityapps/playback/chromecast/CastSessionManager.kt +++ b/android/playback/src/main/java/com/simplecityapps/playback/chromecast/CastSessionManager.kt @@ -35,7 +35,9 @@ constructor( override fun onSessionStarting(castSession: CastSession) { Timber.d("onSessionStarting") - httpServer.start() + if (!httpServer.isAlive) { + httpServer.start() + } } override fun onSessionStarted( @@ -53,6 +55,7 @@ constructor( i: Int ) { Timber.e("onSessionStartFailed") + httpServer.stop() } override fun onSessionResuming( @@ -60,7 +63,7 @@ constructor( s: String ) { Timber.d("onSessionResuming") - if (!httpServer.wasStarted()) { + if (!httpServer.isAlive) { httpServer.start() } } @@ -83,6 +86,7 @@ constructor( i: Int ) { Timber.e("onSessionResumeFailed ($i)") + httpServer.stop() } override fun onSessionSuspended( From b25f9f5e441ee3f5d5d1edeeb84a0e5d8bc501f4 Mon Sep 17 00:00:00 2001 From: Tim Malseed Date: Thu, 15 Jan 2026 22:37:49 +1100 Subject: [PATCH 6/9] Fixed a concurrent modification exception --- .../java/com/simplecityapps/playback/PlaybackWatcher.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/android/playback/src/main/java/com/simplecityapps/playback/PlaybackWatcher.kt b/android/playback/src/main/java/com/simplecityapps/playback/PlaybackWatcher.kt index c6fbce5f1..3915f533e 100644 --- a/android/playback/src/main/java/com/simplecityapps/playback/PlaybackWatcher.kt +++ b/android/playback/src/main/java/com/simplecityapps/playback/PlaybackWatcher.kt @@ -36,16 +36,16 @@ class PlaybackWatcher : PlaybackWatcherCallback { duration: Int, fromUser: Boolean ) { - callbacks.forEach { callback -> callback.onProgressChanged(position, duration, fromUser) } + callbacks.toList().forEach { callback -> callback.onProgressChanged(position, duration, fromUser) } } override fun onPlaybackStateChanged(playbackState: PlaybackState) { Timber.v("onPlaybackStateChanged(playbackState: $playbackState)") - callbacks.forEach { callback -> callback.onPlaybackStateChanged(playbackState) } + callbacks.toList().forEach { callback -> callback.onPlaybackStateChanged(playbackState) } } override fun onTrackEnded(song: Song) { Timber.v("onTrackEnded(song: ${song.name})") - callbacks.forEach { callback -> callback.onTrackEnded(song) } + callbacks.toList().forEach { callback -> callback.onTrackEnded(song) } } } From 596c185b24c689f3435190cfc19a38d53ff48f1a Mon Sep 17 00:00:00 2001 From: Tim Malseed Date: Thu, 15 Jan 2026 22:49:45 +1100 Subject: [PATCH 7/9] Various possible crash fixes --- .../simplecityapps/shuttle/ui/MainActivity.kt | 4 ++-- .../ui/common/dialog/TagEditorAlertDialog.kt | 4 +++- .../albumartists/AlbumArtistListFragment.kt | 20 ++++++++++++------- .../shuttle/ui/screens/main/MainFragment.kt | 3 ++- .../onboarding/OnboardingParentFragment.kt | 5 ++++- .../shuttle/parcel/LocalDateParceler.kt | 2 +- .../playback/PlaybackService.kt | 11 ++++++++-- .../playback/queue/QueueManager.kt | 10 ++++++++-- 8 files changed, 42 insertions(+), 17 deletions(-) diff --git a/android/app/src/main/java/com/simplecityapps/shuttle/ui/MainActivity.kt b/android/app/src/main/java/com/simplecityapps/shuttle/ui/MainActivity.kt index a63650811..2065cc532 100644 --- a/android/app/src/main/java/com/simplecityapps/shuttle/ui/MainActivity.kt +++ b/android/app/src/main/java/com/simplecityapps/shuttle/ui/MainActivity.kt @@ -43,7 +43,7 @@ class MainActivity : AppCompatActivity() { @AppCoroutineScope lateinit var scope: CoroutineScope - lateinit var snowfallView: SnowfallView + var snowfallView: SnowfallView? = null // Lifecycle @@ -78,7 +78,7 @@ class MainActivity : AppCompatActivity() { withTimeout(5000) { remoteConfig.fetchAndActivate().await() } - snowfallView.setForecast(remoteConfig.getDouble("snow_forecast")) + snowfallView?.setForecast(remoteConfig.getDouble("snow_forecast")) } } diff --git a/android/app/src/main/java/com/simplecityapps/shuttle/ui/common/dialog/TagEditorAlertDialog.kt b/android/app/src/main/java/com/simplecityapps/shuttle/ui/common/dialog/TagEditorAlertDialog.kt index 986d694bf..b4cbcfa3c 100644 --- a/android/app/src/main/java/com/simplecityapps/shuttle/ui/common/dialog/TagEditorAlertDialog.kt +++ b/android/app/src/main/java/com/simplecityapps/shuttle/ui/common/dialog/TagEditorAlertDialog.kt @@ -167,7 +167,9 @@ class TagEditorAlertDialog : } fun show(manager: FragmentManager) { - super.show(manager, TAG) + if (!manager.isStateSaved) { + super.show(manager, TAG) + } } // Private diff --git a/android/app/src/main/java/com/simplecityapps/shuttle/ui/screens/library/albumartists/AlbumArtistListFragment.kt b/android/app/src/main/java/com/simplecityapps/shuttle/ui/screens/library/albumartists/AlbumArtistListFragment.kt index 034b78d40..58d87ebe9 100644 --- a/android/app/src/main/java/com/simplecityapps/shuttle/ui/screens/library/albumartists/AlbumArtistListFragment.kt +++ b/android/app/src/main/java/com/simplecityapps/shuttle/ui/screens/library/albumartists/AlbumArtistListFragment.kt @@ -46,6 +46,7 @@ import com.simplecityapps.shuttle.ui.screens.playlistmenu.PlaylistMenuView import com.squareup.phrase.Phrase import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject +import timber.log.Timber @AndroidEntryPoint class AlbumArtistListFragment : @@ -336,13 +337,18 @@ class AlbumArtistListFragment : viewHolder: AlbumArtistBinder.ViewHolder ) { if (!contextualToolbarHelper.handleClick(albumArtist)) { - if (findNavController().currentDestination?.id != R.id.albumArtistDetailFragment) { - findNavController().navigate( - R.id.action_libraryFragment_to_albumArtistDetailFragment, - AlbumArtistDetailFragmentArgs(albumArtist).toBundle(), - null, - FragmentNavigatorExtras(viewHolder.imageView to viewHolder.imageView.transitionName) - ) + // Verify we're on libraryFragment (where the action is defined) before navigating + if (findNavController().currentDestination?.id == R.id.libraryFragment) { + try { + findNavController().navigate( + R.id.action_libraryFragment_to_albumArtistDetailFragment, + AlbumArtistDetailFragmentArgs(albumArtist).toBundle(), + null, + FragmentNavigatorExtras(viewHolder.imageView to viewHolder.imageView.transitionName) + ) + } catch (e: IllegalArgumentException) { + Timber.e(e, "Failed to navigate to album artist detail") + } } } } diff --git a/android/app/src/main/java/com/simplecityapps/shuttle/ui/screens/main/MainFragment.kt b/android/app/src/main/java/com/simplecityapps/shuttle/ui/screens/main/MainFragment.kt index 8e078c06f..7c2bf5a25 100644 --- a/android/app/src/main/java/com/simplecityapps/shuttle/ui/screens/main/MainFragment.kt +++ b/android/app/src/main/java/com/simplecityapps/shuttle/ui/screens/main/MainFragment.kt @@ -165,7 +165,8 @@ class MainFragment : if (task.isSuccessful) { // We got the ReviewInfo object val reviewInfo = task.result - reviewManager.launchReviewFlow(requireActivity(), reviewInfo) + // Use nullable activity since fragment may be detached when async callback fires + activity?.let { reviewManager.launchReviewFlow(it, reviewInfo) } } else { // There was some problem, log or handle the error code. Timber.e(task.exception ?: Exception("Unknown"), "Failed to launch review flow") diff --git a/android/app/src/main/java/com/simplecityapps/shuttle/ui/screens/onboarding/OnboardingParentFragment.kt b/android/app/src/main/java/com/simplecityapps/shuttle/ui/screens/onboarding/OnboardingParentFragment.kt index 6cf3cfa99..35e1522dc 100644 --- a/android/app/src/main/java/com/simplecityapps/shuttle/ui/screens/onboarding/OnboardingParentFragment.kt +++ b/android/app/src/main/java/com/simplecityapps/shuttle/ui/screens/onboarding/OnboardingParentFragment.kt @@ -196,7 +196,10 @@ class OnboardingParentFragment : generalPreferenceManager.hasOnboarded = true if (args.isOnboarding) { - findNavController().navigate(R.id.action_onboardingFragment_to_mainFragment) + // Guard against double-tap or race conditions where navigation already occurred + if (findNavController().currentDestination?.id == R.id.onboardingFragment) { + findNavController().navigate(R.id.action_onboardingFragment_to_mainFragment) + } } else { findNavController().popBackStack() } diff --git a/android/data/src/main/kotlin/com/simplecityapps/shuttle/parcel/LocalDateParceler.kt b/android/data/src/main/kotlin/com/simplecityapps/shuttle/parcel/LocalDateParceler.kt index 45d8ec410..ca6f94e04 100644 --- a/android/data/src/main/kotlin/com/simplecityapps/shuttle/parcel/LocalDateParceler.kt +++ b/android/data/src/main/kotlin/com/simplecityapps/shuttle/parcel/LocalDateParceler.kt @@ -5,7 +5,7 @@ import kotlinx.datetime.LocalDate import kotlinx.parcelize.Parceler object LocalDateParceler : Parceler { - override fun create(parcel: Parcel) = LocalDate.parse(parcel.readString()!!) + override fun create(parcel: Parcel): LocalDate? = parcel.readString()?.let { LocalDate.parse(it) } override fun LocalDate?.write( parcel: Parcel, diff --git a/android/playback/src/main/java/com/simplecityapps/playback/PlaybackService.kt b/android/playback/src/main/java/com/simplecityapps/playback/PlaybackService.kt index 8b258e86a..ade97806e 100644 --- a/android/playback/src/main/java/com/simplecityapps/playback/PlaybackService.kt +++ b/android/playback/src/main/java/com/simplecityapps/playback/PlaybackService.kt @@ -93,7 +93,7 @@ class PlaybackService : Timber.v("onStartCommand() action: ${intent?.action}") - if (intent == null && (playbackManager.playbackState() != PlaybackState.Loading || playbackManager.playbackState() != PlaybackState.Playing)) { + if (intent == null && (playbackManager.playbackState() != PlaybackState.Loading && playbackManager.playbackState() != PlaybackState.Playing)) { stopForeground(true) return START_NOT_STICKY } @@ -106,8 +106,15 @@ class PlaybackService : intent?.let { when (intent.action) { ACTION_NOTIFICATION_DISMISS -> { - // The user has swiped away the notification. This is only possible when the service is no longer running in the foreground + // The user has swiped away the notification. This is only possible when the service is no longer running in the foreground. + // We still need to call startForeground() in case this was started via startForegroundService(). Timber.v("Stopping due to notification dismiss") + val notification = if (queueManager.getQueue().isEmpty()) { + notificationManager.displayQueueEmptyNotification() + } else { + notificationManager.displayPlaybackNotification() + } + startForegroundSafely(notification) stopSelf() return START_NOT_STICKY } diff --git a/android/playback/src/main/java/com/simplecityapps/playback/queue/QueueManager.kt b/android/playback/src/main/java/com/simplecityapps/playback/queue/QueueManager.kt index ac9f963e2..2424ae41d 100644 --- a/android/playback/src/main/java/com/simplecityapps/playback/queue/QueueManager.kt +++ b/android/playback/src/main/java/com/simplecityapps/playback/queue/QueueManager.kt @@ -398,10 +398,16 @@ class QueueManager( new: QueueItem ) { if (baseList.isNotEmpty()) { - baseList[baseList.indexOf(old)] = new + val baseIndex = baseList.indexOf(old) + if (baseIndex != -1) { + baseList[baseIndex] = new + } } if (shuffleList.isNotEmpty()) { - shuffleList[shuffleList.indexOf(old)] = new + val shuffleIndex = shuffleList.indexOf(old) + if (shuffleIndex != -1) { + shuffleList[shuffleIndex] = new + } } } From 0bef12997c81c8787534975b2770b1c36e25f900 Mon Sep 17 00:00:00 2001 From: Tim Malseed Date: Thu, 15 Jan 2026 22:50:26 +1100 Subject: [PATCH 8/9] Lint --- .../src/main/java/com/simplecityapps/shuttle/ui/MainActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/app/src/main/java/com/simplecityapps/shuttle/ui/MainActivity.kt b/android/app/src/main/java/com/simplecityapps/shuttle/ui/MainActivity.kt index 2065cc532..72a5a71dc 100644 --- a/android/app/src/main/java/com/simplecityapps/shuttle/ui/MainActivity.kt +++ b/android/app/src/main/java/com/simplecityapps/shuttle/ui/MainActivity.kt @@ -7,7 +7,6 @@ import android.content.pm.PackageManager import android.os.Build import android.os.Bundle import android.provider.MediaStore -import timber.log.Timber import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import androidx.navigation.fragment.NavHostFragment @@ -24,6 +23,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.tasks.await import kotlinx.coroutines.withTimeout +import timber.log.Timber @AndroidEntryPoint class MainActivity : AppCompatActivity() { From f27150cc42f09d47373c509f82ee36eb19adfc9a Mon Sep 17 00:00:00 2001 From: Tim Malseed Date: Thu, 15 Jan 2026 23:04:12 +1100 Subject: [PATCH 9/9] bump to 1.0.9 --- android/app/src/main/assets/changelog.json | 17 +++++++++++++++++ buildSrc/src/main/kotlin/AppVersion.kt | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/android/app/src/main/assets/changelog.json b/android/app/src/main/assets/changelog.json index 796cc0884..815db1d90 100644 --- a/android/app/src/main/assets/changelog.json +++ b/android/app/src/main/assets/changelog.json @@ -1,4 +1,21 @@ [ + { + "versionName": "1.0.9", + "releaseDate": "15/01/2026", + "features": [ + + ], + "fixes": [ + "Fixed Chromecast reliability issues", + "Fixed a long-standing issue where the app could crash when playing from the background", + "Various stability improvements" + ], + "improvements": [ + ], + "notes": [ + "What's up? I want to make Shuttle great again. I just need more hours in the day, and more days in the week!" + ] + }, { "versionName": "1.0.8", "releaseDate": "10/01/2026", diff --git a/buildSrc/src/main/kotlin/AppVersion.kt b/buildSrc/src/main/kotlin/AppVersion.kt index bf21f49fc..200b52e54 100644 --- a/buildSrc/src/main/kotlin/AppVersion.kt +++ b/buildSrc/src/main/kotlin/AppVersion.kt @@ -1,6 +1,6 @@ object AppVersion { const val versionMajor = 1 const val versionMinor = 0 - const val versionPatch = 8 + const val versionPatch = 9 val versionSuffix: String? = "" }