diff --git a/automotive/src/main/java/com/chamika/dashtune/AlbumArtContentProvider.kt b/automotive/src/main/java/com/chamika/dashtune/AlbumArtContentProvider.kt index ca5662b..74be0a8 100644 --- a/automotive/src/main/java/com/chamika/dashtune/AlbumArtContentProvider.kt +++ b/automotive/src/main/java/com/chamika/dashtune/AlbumArtContentProvider.kt @@ -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 @@ -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() diff --git a/automotive/src/main/java/com/chamika/dashtune/DashTuneApplication.kt b/automotive/src/main/java/com/chamika/dashtune/DashTuneApplication.kt index 03afaeb..4108289 100644 --- a/automotive/src/main/java/com/chamika/dashtune/DashTuneApplication.kt +++ b/automotive/src/main/java/com/chamika/dashtune/DashTuneApplication.kt @@ -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 @@ -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) } } } diff --git a/automotive/src/main/java/com/chamika/dashtune/DashTuneMusicService.kt b/automotive/src/main/java/com/chamika/dashtune/DashTuneMusicService.kt index 205028c..53f5f73 100644 --- a/automotive/src/main/java/com/chamika/dashtune/DashTuneMusicService.kt +++ b/automotive/src/main/java/com/chamika/dashtune/DashTuneMusicService.kt @@ -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 @@ -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) @@ -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 { @@ -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) } @@ -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) @@ -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) { @@ -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)") } } } @@ -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) @@ -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 @@ -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) } } } diff --git a/automotive/src/main/java/com/chamika/dashtune/DashTuneSessionCallback.kt b/automotive/src/main/java/com/chamika/dashtune/DashTuneSessionCallback.kt index c6450c2..fe8b352 100644 --- a/automotive/src/main/java/com/chamika/dashtune/DashTuneSessionCallback.kt +++ b/automotive/src/main/java/com/chamika/dashtune/DashTuneSessionCallback.kt @@ -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 @@ -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, "")) } } @@ -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")) } } @@ -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")) } } @@ -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() } } @@ -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) } } @@ -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, @@ -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, @@ -427,8 +426,8 @@ 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) } } @@ -436,9 +435,9 @@ class DashTuneSessionCallback( 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 } diff --git a/automotive/src/main/java/com/chamika/dashtune/FirebaseUtils.kt b/automotive/src/main/java/com/chamika/dashtune/FirebaseUtils.kt new file mode 100644 index 0000000..8aaa005 --- /dev/null +++ b/automotive/src/main/java/com/chamika/dashtune/FirebaseUtils.kt @@ -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) + } + } +} 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 aa42cf2..c669391 100644 --- a/automotive/src/main/java/com/chamika/dashtune/data/MediaRepository.kt +++ b/automotive/src/main/java/com/chamika/dashtune/data/MediaRepository.kt @@ -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 @@ -80,7 +80,7 @@ class MediaRepository( val allEntities = mutableListOf() var anySuccess = false - FirebaseCrashlytics.getInstance().log("Sync started: ${sectionIds.size} sections") + FirebaseUtils.safeLog("Sync started: ${sectionIds.size} sections") for (sectionId in sectionIds) { try { @@ -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) } } @@ -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 } diff --git a/automotive/src/main/java/com/chamika/dashtune/signin/SignInViewModel.kt b/automotive/src/main/java/com/chamika/dashtune/signin/SignInViewModel.kt index 94d6b14..e0cc6cf 100644 --- a/automotive/src/main/java/com/chamika/dashtune/signin/SignInViewModel.kt +++ b/automotive/src/main/java/com/chamika/dashtune/signin/SignInViewModel.kt @@ -6,7 +6,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.chamika.dashtune.Constants.LOG_TAG -import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.chamika.dashtune.FirebaseUtils import com.chamika.dashtune.auth.JellyfinAccountManager import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers @@ -50,8 +50,8 @@ class SignInViewModel @Inject constructor() : ViewModel() { } catch (e: Exception) { Log.w(LOG_TAG, "Error", e) val host = try { java.net.URI(serverUrl).host ?: "unknown" } catch (_: Exception) { "invalid_url" } - FirebaseCrashlytics.getInstance().setCustomKey("server_url_host", host) - FirebaseCrashlytics.getInstance().recordException(e) + FirebaseUtils.safeSetCustomKey("server_url_host", host) + FirebaseUtils.safeRecordException(e) false } } @@ -105,8 +105,8 @@ class SignInViewModel @Inject constructor() : ViewModel() { } if (loginResponse.status == 200) { - FirebaseCrashlytics.getInstance().setCustomKey("auth_method", "quick_connect") - FirebaseCrashlytics.getInstance().log("Login successful via quick_connect") + FirebaseUtils.safeSetCustomKey("auth_method", "quick_connect") + FirebaseUtils.safeLog("Login successful via quick_connect") loginSuccess( server, loginResponse.content.user?.name!!, @@ -123,16 +123,16 @@ class SignInViewModel @Inject constructor() : ViewModel() { } if (response.status == 200) { - FirebaseCrashlytics.getInstance().setCustomKey("auth_method", "password") - FirebaseCrashlytics.getInstance().log("Login successful via password") + FirebaseUtils.safeSetCustomKey("auth_method", "password") + FirebaseUtils.safeLog("Login successful via password") loginSuccess(server, username, response.content.accessToken!!) } response.status == 200 } catch (e: Exception) { Log.e(LOG_TAG, "Error", e) - FirebaseCrashlytics.getInstance().setCustomKey("auth_method", "password") - FirebaseCrashlytics.getInstance().recordException(e) + FirebaseUtils.safeSetCustomKey("auth_method", "password") + FirebaseUtils.safeRecordException(e) false } }