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
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import android.net.Uri
import android.os.ParcelFileDescriptor
import android.util.Log
import com.chamika.dashtune.Constants.LOG_TAG
import com.google.firebase.crashlytics.FirebaseCrashlytics
import okhttp3.OkHttpClient
import okhttp3.Request
import okio.buffer
Expand Down Expand Up @@ -101,8 +100,8 @@ class AlbumArtContentProvider : ContentProvider() {
tmpFile.renameTo(file)
} else {
Log.w(LOG_TAG, "Failed to download $remoteUri: \n ${it.code} - ${it.body}")
FirebaseCrashlytics.getInstance().setCustomKey("album_art_path", remoteUri.path ?: "unknown")
FirebaseCrashlytics.getInstance().recordException(Exception("Album art download failed: HTTP ${it.code}"))
FirebaseUtils.safeSetCustomKey("album_art_path", remoteUri.path ?: "unknown")
FirebaseUtils.safeRecordException(Exception("Album art download failed: HTTP ${it.code}"))
}

inProgress.get(remoteUri)?.countDown()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.chamika.dashtune

import android.app.Application
import android.util.Log
import com.chamika.dashtune.Constants.LOG_TAG
import com.google.firebase.Firebase
import com.google.firebase.analytics.analytics
import com.google.firebase.crashlytics.crashlytics
Expand All @@ -11,10 +13,18 @@ class DashTuneApplication : Application() {
override fun onCreate() {
super.onCreate()

// Disable Firebase collection in debug builds
if (BuildConfig.DEBUG) {
Firebase.analytics.setAnalyticsCollectionEnabled(false)
Firebase.crashlytics.setCrashlyticsCollectionEnabled(false)
// Disable Firebase collection in debug builds.
// Wrapped in try-catch because Firebase depends on Google Play Services, which may not
// yet be ready shortly after a car software update (GMS can take several minutes to
// initialise on first boot after an OTA). Crashing here would prevent the media service
// from starting and cause the AAOS "Something went wrong" error dialog.
try {
if (BuildConfig.DEBUG) {
Firebase.analytics.setAnalyticsCollectionEnabled(false)
Firebase.crashlytics.setCrashlyticsCollectionEnabled(false)
}
} catch (e: Exception) {
Log.w(LOG_TAG, "Firebase init skipped – Google Play Services not ready yet", e)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ import androidx.media3.session.MediaLibraryService
import androidx.media3.session.MediaSession
import androidx.preference.PreferenceManager
import com.chamika.dashtune.Constants.LOG_TAG
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.chamika.dashtune.DashTuneSessionCallback.Companion.PLAYLIST_INDEX_PREF
import com.chamika.dashtune.DashTuneSessionCallback.Companion.PLAYLIST_TRACK_POSITON_MS_PREF
import com.chamika.dashtune.data.db.MediaCacheDao
Expand Down Expand Up @@ -97,7 +96,7 @@ class DashTuneMusicService : MediaLibraryService() {
super.onCreate()

Log.i(LOG_TAG, "onCreate")
FirebaseCrashlytics.getInstance().log("Service created")
FirebaseUtils.safeLog("Service created")

accountManager = com.chamika.dashtune.auth.JellyfinAccountManager(
AccountManager.get(applicationContext)
Expand Down Expand Up @@ -155,12 +154,12 @@ class DashTuneMusicService : MediaLibraryService() {
prefetchNextTracks(player, prefetchCount)
}

FirebaseCrashlytics.getInstance().setCustomKey("current_track_id", player.currentMediaItem?.mediaId ?: "")
FirebaseCrashlytics.getInstance().setCustomKey("playlist_size", player.mediaItemCount)
FirebaseUtils.safeSetCustomKey("current_track_id", player.currentMediaItem?.mediaId ?: "")
FirebaseUtils.safeSetCustomKey("playlist_size", player.mediaItemCount)
}

if (events.contains(Player.EVENT_IS_PLAYING_CHANGED)) {
FirebaseCrashlytics.getInstance().setCustomKey("is_playing", player.isPlaying)
FirebaseUtils.safeSetCustomKey("is_playing", player.isPlaying)
if (player.isPlaying) {
startPlaybackPoll()
} else {
Expand All @@ -169,14 +168,14 @@ class DashTuneMusicService : MediaLibraryService() {
}

if (events.contains(Player.EVENT_REPEAT_MODE_CHANGED)) {
FirebaseCrashlytics.getInstance().setCustomKey("repeat_mode", player.repeatMode)
FirebaseUtils.safeSetCustomKey("repeat_mode", player.repeatMode)
PreferenceManager.getDefaultSharedPreferences(this@DashTuneMusicService).edit {
putInt("repeat_mode", player.repeatMode)
}
}

if (events.contains(Player.EVENT_SHUFFLE_MODE_ENABLED_CHANGED)) {
FirebaseCrashlytics.getInstance().setCustomKey("shuffle_enabled", player.shuffleModeEnabled)
FirebaseUtils.safeSetCustomKey("shuffle_enabled", player.shuffleModeEnabled)
PreferenceManager.getDefaultSharedPreferences(this@DashTuneMusicService).edit {
putBoolean("shuffle_enabled", player.shuffleModeEnabled)
}
Expand Down Expand Up @@ -226,7 +225,7 @@ class DashTuneMusicService : MediaLibraryService() {
networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
Log.i(LOG_TAG, "Network available")
FirebaseCrashlytics.getInstance().log("Network available")
FirebaseUtils.safeLog("Network available")
if (!accountManager.isAuthenticated) return

val prefs = PreferenceManager.getDefaultSharedPreferences(this@DashTuneMusicService)
Expand All @@ -235,7 +234,7 @@ class DashTuneMusicService : MediaLibraryService() {
val syncNeeded = System.currentTimeMillis() - lastSync > sixHoursMs

if (syncNeeded) {
FirebaseCrashlytics.getInstance().log("Network available: sync triggered")
FirebaseUtils.safeLog("Network available: sync triggered")
serviceScope.launch {
val success = callback.sync()
if (success) {
Expand All @@ -249,7 +248,7 @@ class DashTuneMusicService : MediaLibraryService() {
}
}
} else {
FirebaseCrashlytics.getInstance().log("Network available: sync skipped (ran recently)")
FirebaseUtils.safeLog("Network available: sync skipped (ran recently)")
}
}
}
Expand Down Expand Up @@ -284,7 +283,7 @@ class DashTuneMusicService : MediaLibraryService() {

override fun onDestroy() {
Log.i(LOG_TAG, "onDestroy")
FirebaseCrashlytics.getInstance().log("Service destroyed")
FirebaseUtils.safeLog("Service destroyed")

mediaLibrarySession.release()
mediaLibrarySession.player.removeListener(playerListener)
Expand Down Expand Up @@ -325,7 +324,7 @@ class DashTuneMusicService : MediaLibraryService() {
}

fun onLogin() {
FirebaseCrashlytics.getInstance().log("Login applied to service")
FirebaseUtils.safeLog("Login applied to service")
jellyfinApi.update(
baseUrl = accountManager.server,
accessToken = accountManager.token
Expand Down Expand Up @@ -378,8 +377,8 @@ class DashTuneMusicService : MediaLibraryService() {
downloadManager.resumeDownloads()
} catch (e: Exception) {
Log.w(LOG_TAG, "Failed to prefetch: $id", e)
FirebaseCrashlytics.getInstance().setCustomKey("prefetch_track_id", id)
FirebaseCrashlytics.getInstance().recordException(e)
FirebaseUtils.safeSetCustomKey("prefetch_track_id", id)
FirebaseUtils.safeRecordException(e)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import androidx.media3.session.SessionError
import androidx.media3.session.SessionResult
import androidx.preference.PreferenceManager
import com.chamika.dashtune.Constants.LOG_TAG
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.chamika.dashtune.auth.JellyfinAccountManager
import com.chamika.dashtune.data.MediaRepository
import com.chamika.dashtune.data.db.MediaCacheDao
Expand Down Expand Up @@ -115,8 +114,8 @@ class DashTuneSessionCallback(
)
} catch (e: Exception) {
Log.e(LOG_TAG, "Failed to get library root", e)
FirebaseCrashlytics.getInstance().setCustomKey("failed_operation", "get_library_root")
FirebaseCrashlytics.getInstance().recordException(e)
FirebaseUtils.safeSetCustomKey("failed_operation", "get_library_root")
FirebaseUtils.safeRecordException(e)
LibraryResult.ofError(SessionError(SessionError.ERROR_UNKNOWN, ""))
}
}
Expand Down Expand Up @@ -149,9 +148,9 @@ class DashTuneSessionCallback(
LibraryResult.ofItemList(repository.getChildren(parentId), params)
} catch (e: Exception) {
Log.e(LOG_TAG, "Failed to get children for $parentId", e)
FirebaseCrashlytics.getInstance().setCustomKey("failed_operation", "get_children")
FirebaseCrashlytics.getInstance().setCustomKey("parent_id", parentId)
FirebaseCrashlytics.getInstance().recordException(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"))
}
}
Expand Down Expand Up @@ -193,8 +192,8 @@ class DashTuneSessionCallback(
)
} catch (e: Exception) {
Log.e(LOG_TAG, "Failed to get item $mediaId", e)
FirebaseCrashlytics.getInstance().setCustomKey("failed_operation", "get_item")
FirebaseCrashlytics.getInstance().recordException(e)
FirebaseUtils.safeSetCustomKey("failed_operation", "get_item")
FirebaseUtils.safeRecordException(e)
LibraryResult.ofError(SessionError(SessionError.ERROR_UNKNOWN, "Failed to get media item"))
}
}
Expand Down Expand Up @@ -293,9 +292,9 @@ class DashTuneSessionCallback(
LibraryResult.ofVoid(params)
} catch (e: Exception) {
Log.e(LOG_TAG, "Failed to search for '$query'", e)
FirebaseCrashlytics.getInstance().setCustomKey("failed_operation", "search")
FirebaseCrashlytics.getInstance().setCustomKey("search_query", query)
FirebaseCrashlytics.getInstance().recordException(e)
FirebaseUtils.safeSetCustomKey("failed_operation", "search")
FirebaseUtils.safeSetCustomKey("search_query", query)
FirebaseUtils.safeRecordException(e)
LibraryResult.ofVoid()
}
}
Expand All @@ -315,9 +314,9 @@ class DashTuneSessionCallback(
LibraryResult.ofItemList(results, params)
} catch (e: Exception) {
Log.e(LOG_TAG, "Failed to get search results for '$query'", e)
FirebaseCrashlytics.getInstance().setCustomKey("failed_operation", "get_search_result")
FirebaseCrashlytics.getInstance().setCustomKey("search_query", query)
FirebaseCrashlytics.getInstance().recordException(e)
FirebaseUtils.safeSetCustomKey("failed_operation", "get_search_result")
FirebaseUtils.safeSetCustomKey("search_query", query)
FirebaseUtils.safeRecordException(e)
LibraryResult.ofItemList(emptyList(), params)
}
}
Expand All @@ -343,7 +342,7 @@ class DashTuneSessionCallback(
?.awaitAll() ?: listOf()

Log.d(LOG_TAG, "Resuming playback with $mediaItemsToRestore")
FirebaseCrashlytics.getInstance().log("Restoring playback: index=${prefs.getInt(PLAYLIST_INDEX_PREF, 0)}, trackCount=${mediaItemsToRestore.size}")
FirebaseUtils.safeLog("Restoring playback: index=${prefs.getInt(PLAYLIST_INDEX_PREF, 0)}, trackCount=${mediaItemsToRestore.size}")

MediaSession.MediaItemsWithStartPosition(
mediaItemsToRestore,
Expand All @@ -352,7 +351,7 @@ class DashTuneSessionCallback(
)
} catch (e: Exception) {
Log.e(LOG_TAG, "Failed to resume playback", e)
FirebaseCrashlytics.getInstance().recordException(e)
FirebaseUtils.safeRecordException(e)
MediaSession.MediaItemsWithStartPosition(
emptyList(),
0,
Expand Down Expand Up @@ -427,18 +426,18 @@ class DashTuneSessionCallback(
SessionResult(SessionResult.RESULT_SUCCESS)
} catch (e: Exception) {
Log.e(LOG_TAG, "Failed to set rating for $mediaId", e)
FirebaseCrashlytics.getInstance().setCustomKey("failed_operation", "set_rating")
FirebaseCrashlytics.getInstance().recordException(e)
FirebaseUtils.safeSetCustomKey("failed_operation", "set_rating")
FirebaseUtils.safeRecordException(e)
SessionResult(SessionError.ERROR_UNKNOWN)
}
}
}

suspend fun sync(): Boolean {
if (!::repository.isInitialized) return false
FirebaseCrashlytics.getInstance().log("Sync started")
FirebaseUtils.safeLog("Sync started")
val result = repository.sync()
FirebaseCrashlytics.getInstance().log(if (result) "Sync completed" else "Sync failed")
FirebaseUtils.safeLog(if (result) "Sync completed" else "Sync failed")
return result
}

Expand Down
56 changes: 56 additions & 0 deletions automotive/src/main/java/com/chamika/dashtune/FirebaseUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.chamika.dashtune

import android.util.Log
import com.chamika.dashtune.Constants.LOG_TAG
import com.google.firebase.crashlytics.FirebaseCrashlytics

object FirebaseUtils {

/**
* Records [e] to Firebase Crashlytics, swallowing any exception that might occur if Google
* Play Services is not yet available (e.g. shortly after a car OTA update).
*/
fun safeRecordException(e: Exception) {
try {
FirebaseCrashlytics.getInstance().recordException(e)
} catch (crashlyticsError: Exception) {
Log.w(LOG_TAG, "Crashlytics unavailable – Google Play Services not ready yet", crashlyticsError)
}
}

/** Logs [message] to Firebase Crashlytics, safe against GMS unavailability. */
fun safeLog(message: String) {
try {
FirebaseCrashlytics.getInstance().log(message)
} catch (e: Exception) {
Log.w(LOG_TAG, "Crashlytics unavailable – Google Play Services not ready yet", e)
}
}

/** Sets a string custom key on Firebase Crashlytics, safe against GMS unavailability. */
fun safeSetCustomKey(key: String, value: String) {
try {
FirebaseCrashlytics.getInstance().setCustomKey(key, value)
} catch (e: Exception) {
Log.w(LOG_TAG, "Crashlytics unavailable – Google Play Services not ready yet", e)
}
}

/** Sets an int custom key on Firebase Crashlytics, safe against GMS unavailability. */
fun safeSetCustomKey(key: String, value: Int) {
try {
FirebaseCrashlytics.getInstance().setCustomKey(key, value)
} catch (e: Exception) {
Log.w(LOG_TAG, "Crashlytics unavailable – Google Play Services not ready yet", e)
}
}

/** Sets a boolean custom key on Firebase Crashlytics, safe against GMS unavailability. */
fun safeSetCustomKey(key: String, value: Boolean) {
try {
FirebaseCrashlytics.getInstance().setCustomKey(key, value)
} catch (e: Exception) {
Log.w(LOG_TAG, "Crashlytics unavailable – Google Play Services not ready yet", e)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import com.chamika.dashtune.media.MediaItemFactory.Companion.LATEST_ALBUMS
import com.chamika.dashtune.media.MediaItemFactory.Companion.PLAYLISTS
import com.chamika.dashtune.media.MediaItemFactory.Companion.RANDOM_ALBUMS
import com.chamika.dashtune.media.MediaItemFactory.Companion.ROOT_ID
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.chamika.dashtune.FirebaseUtils
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.json.JSONObject
Expand Down Expand Up @@ -80,7 +80,7 @@ class MediaRepository(
val allEntities = mutableListOf<CachedMediaItemEntity>()
var anySuccess = false

FirebaseCrashlytics.getInstance().log("Sync started: ${sectionIds.size} sections")
FirebaseUtils.safeLog("Sync started: ${sectionIds.size} sections")

for (sectionId in sectionIds) {
try {
Expand All @@ -92,8 +92,8 @@ class MediaRepository(
anySuccess = true
} catch (e: Exception) {
Log.e(LOG_TAG, "Failed to sync section $sectionId", e)
FirebaseCrashlytics.getInstance().setCustomKey("sync_failed_section", sectionId)
FirebaseCrashlytics.getInstance().recordException(e)
FirebaseUtils.safeSetCustomKey("sync_failed_section", sectionId)
FirebaseUtils.safeRecordException(e)
}
}

Expand All @@ -102,7 +102,7 @@ class MediaRepository(
dao.insertAll(allEntities)
}

FirebaseCrashlytics.getInstance().log("Sync completed: ${allEntities.size} items, success=$anySuccess")
FirebaseUtils.safeLog("Sync completed: ${allEntities.size} items, success=$anySuccess")
return anySuccess
}

Expand Down
Loading
Loading