From 0577a7d53659036092462e93926b236e864b58c9 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Wed, 24 Jun 2026 22:15:59 +0800 Subject: [PATCH 1/8] fix(fcm): skip session update without token --- app/build.gradle.kts | 1 + .../one/mixin/android/worker/SessionWorker.kt | 37 +++++++++++++------ 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6330a36eca..0005b08937 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -460,6 +460,7 @@ dependencies { implementation("org.jetbrains.kotlin:kotlin-stdlib:2.4.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:$coroutinesVersion") implementation("com.github.zjupure:webpdecoder:$webpdecoderVersion") implementation("com.github.bumptech.glide:glide:$glideVersion") implementation("com.github.bumptech.glide:okhttp3-integration:$glideVersion") diff --git a/app/src/main/java/one/mixin/android/worker/SessionWorker.kt b/app/src/main/java/one/mixin/android/worker/SessionWorker.kt index 2d603b93f4..0d0729a4cd 100644 --- a/app/src/main/java/one/mixin/android/worker/SessionWorker.kt +++ b/app/src/main/java/one/mixin/android/worker/SessionWorker.kt @@ -3,14 +3,16 @@ package one.mixin.android.worker import android.content.Context import androidx.hilt.work.HiltWorker import androidx.work.WorkerParameters -import com.google.android.gms.tasks.Tasks import com.google.firebase.messaging.FirebaseMessaging import dagger.assisted.Assisted import dagger.assisted.AssistedInject +import kotlinx.coroutines.tasks.await import one.mixin.android.api.request.SessionRequest import one.mixin.android.api.service.AccountService +import one.mixin.android.extension.isGooglePlayServicesAvailable import one.mixin.android.session.Session import one.mixin.android.util.ErrorHandler.Companion.SERVER +import one.mixin.android.util.reportException import timber.log.Timber @HiltWorker @@ -19,6 +21,7 @@ class SessionWorker @AssistedInject constructor( @Assisted parameters: WorkerParameters, private val accountService: AccountService, ) : BaseWork(context, parameters) { + private val appContext = context.applicationContext override suspend fun onRun(): Result { Timber.e("SessionWorker started") @@ -27,14 +30,30 @@ class SessionWorker @AssistedInject constructor( Timber.w("Session update failed: No active account") return Result.failure() } + if (!appContext.isGooglePlayServicesAvailable()) { + Timber.w("Session update skipped: Google Play services unavailable") + return Result.success() + } - val token = retrieveFirebaseToken() - Timber.e("Firebase token retrieved: ${token != null}") + val token = try { + retrieveFirebaseToken() + } catch (e: Exception) { + Timber.e(e, "Failed to retrieve Firebase token, retrying session update") + reportException(IllegalStateException("SessionWorker failed to retrieve Firebase token", e)) + return Result.retry() + } + if (token.isBlank()) { + val error = IllegalStateException("SessionWorker retrieved blank Firebase token") + Timber.e(error, "Failed to retrieve Firebase token, retrying session update") + reportException(error) + return Result.retry() + } + Timber.e("Firebase token retrieved: true") return try { val response = accountService.updateSession(SessionRequest(notificationToken = token)) if (response.isSuccess) { - Timber.e("Session updated successfully") + Timber.e("Session updated successfully with Firebase token") Result.success() } else if (response.errorCode >= SERVER) { Timber.e("Session update failed with server error, retrying...") @@ -49,11 +68,5 @@ class SessionWorker @AssistedInject constructor( } } - private fun retrieveFirebaseToken(): String? { - return runCatching { - Tasks.await(FirebaseMessaging.getInstance().token) - }.onFailure { error -> - Timber.e(error, "Failed to retrieve Firebase token") - }.getOrDefault(null) - } -} \ No newline at end of file + private suspend fun retrieveFirebaseToken(): String = FirebaseMessaging.getInstance().token.await() +} From e11e894769f492d0a2e0592bf728cb1e72b402b2 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Wed, 24 Jun 2026 23:19:01 +0800 Subject: [PATCH 2/8] feat(debug): add manual fcm token update --- .../android/ui/setting/LogAndDebugFragment.kt | 39 +++++++++++++ .../ui/setting/LogAndDebugViewModel.kt | 58 ++++++++++++++++++- .../main/res/layout/fragment_log_debug.xml | 12 ++++ app/src/main/res/values-zh-rCN/strings.xml | 2 + app/src/main/res/values-zh-rTW/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + 6 files changed, 114 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/one/mixin/android/ui/setting/LogAndDebugFragment.kt b/app/src/main/java/one/mixin/android/ui/setting/LogAndDebugFragment.kt index a62f01d7e6..97c5af4581 100644 --- a/app/src/main/java/one/mixin/android/ui/setting/LogAndDebugFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/setting/LogAndDebugFragment.kt @@ -24,6 +24,7 @@ import one.mixin.android.db.property.PropertyHelper.updateKeyValue import one.mixin.android.extension.alertDialogBuilder import one.mixin.android.extension.defaultSharedPreferences import one.mixin.android.extension.indeterminateProgressDialog +import one.mixin.android.extension.isGooglePlayServicesAvailable import one.mixin.android.extension.navTo import one.mixin.android.extension.putBoolean import one.mixin.android.extension.toast @@ -100,6 +101,9 @@ class LogAndDebugFragment : BaseFragment(R.layout.fragment_log_debug) { logs.setOnClickListener { shareLogsFile() } + updateFcmToken.setOnClickListener { + updateFcmToken() + } database.setOnClickListener { navTo( DatabaseDebugFragment.newInstance(), @@ -187,6 +191,41 @@ class LogAndDebugFragment : BaseFragment(R.layout.fragment_log_debug) { } } + private fun updateFcmToken() { + val googlePlayServicesAvailable = requireContext().isGooglePlayServicesAvailable() + val progressDialog = indeterminateProgressDialog(message = R.string.Please_wait_a_bit).apply { + setCancelable(false) + } + viewLifecycleOwner.lifecycleScope.launch { + val result = + try { + if (googlePlayServicesAvailable) { + withContext(Dispatchers.IO) { + viewModel.updateFcmToken() + } + } else { + FcmTokenUpdateResult.Failure("Google Play services unavailable") + } + } finally { + progressDialog.dismiss() + } + when (result) { + FcmTokenUpdateResult.Success -> toast(R.string.FCM_Token_Updated) + is FcmTokenUpdateResult.Failure -> showFcmTokenUpdateError(result.message) + } + } + } + + private fun showFcmTokenUpdateError(message: String) { + alertDialogBuilder() + .setTitle(R.string.Update_FCM_Token) + .setMessage(message) + .setPositiveButton(android.R.string.ok) { dialog, _ -> + dialog.dismiss() + } + .show() + } + override fun onDestroyView() { captchaView?.release() captchaView = null diff --git a/app/src/main/java/one/mixin/android/ui/setting/LogAndDebugViewModel.kt b/app/src/main/java/one/mixin/android/ui/setting/LogAndDebugViewModel.kt index d1d9cd2a8a..bf1d949e54 100644 --- a/app/src/main/java/one/mixin/android/ui/setting/LogAndDebugViewModel.kt +++ b/app/src/main/java/one/mixin/android/ui/setting/LogAndDebugViewModel.kt @@ -1,13 +1,20 @@ package one.mixin.android.ui.setting import androidx.lifecycle.ViewModel +import com.google.firebase.messaging.FirebaseMessaging import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.tasks.await +import one.mixin.android.api.request.SessionRequest +import one.mixin.android.api.service.AccountService import one.mixin.android.repository.TokenRepository +import one.mixin.android.session.Session +import timber.log.Timber import javax.inject.Inject @HiltViewModel class LogAndDebugViewModel @Inject constructor( - private val tokenRepository: TokenRepository + private val tokenRepository: TokenRepository, + private val accountService: AccountService, ) : ViewModel() { suspend fun deleteAllWeb3Transactions() { @@ -21,4 +28,53 @@ class LogAndDebugViewModel @Inject constructor( suspend fun deleteAllOrders() { tokenRepository.deleteAllOrders() } + + suspend fun updateFcmToken(): FcmTokenUpdateResult { + Timber.e("Debug FCM token update started") + if (Session.getAccount() == null) { + Timber.w("Debug FCM token update failed: No active account") + return FcmTokenUpdateResult.Failure("No active account") + } + + val token = try { + FirebaseMessaging.getInstance().token.await() + } catch (e: Exception) { + Timber.e(e, "Debug FCM token retrieval failed") + return FcmTokenUpdateResult.Failure("Failed to retrieve Firebase token: ${e.displayMessage()}") + } + if (token.isBlank()) { + Timber.e("Debug FCM token retrieval failed: blank token") + return FcmTokenUpdateResult.Failure("Firebase token is blank") + } + Timber.e("Debug Firebase token retrieved: true") + + return try { + val response = accountService.updateSession(SessionRequest(notificationToken = token)) + if (response.isSuccess) { + Timber.e("Debug session updated successfully with Firebase token") + FcmTokenUpdateResult.Success + } else { + val message = buildString { + append("Session update failed with error code: ${response.errorCode}") + if (response.errorDescription.isNotBlank()) { + append('\n') + append(response.errorDescription) + } + } + Timber.e(message) + FcmTokenUpdateResult.Failure(message) + } + } catch (e: Exception) { + Timber.e(e, "Debug session update failed") + FcmTokenUpdateResult.Failure("Session update failed: ${e.displayMessage()}") + } + } + + private fun Exception.displayMessage() = message ?: javaClass.simpleName +} + +sealed interface FcmTokenUpdateResult { + object Success : FcmTokenUpdateResult + + data class Failure(val message: String) : FcmTokenUpdateResult } diff --git a/app/src/main/res/layout/fragment_log_debug.xml b/app/src/main/res/layout/fragment_log_debug.xml index 75bbc02edc..dae98a561d 100644 --- a/app/src/main/res/layout/fragment_log_debug.xml +++ b/app/src/main/res/layout/fragment_log_debug.xml @@ -50,6 +50,18 @@ android:foreground="?android:attr/selectableItemBackground" android:textSize="16sp" /> + + 分享出错 分享邀请链接 分享日志文件 + 更新 FCM Token + FCM Token 已更新 你确定要发送来自%1$s的%2$s? 你确定要发送该%1$s? 分享二维码 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index ef13d00848..887b322490 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -838,6 +838,8 @@ 分享名片 分享出錯 分享邀請連結 + 更新 FCM Token + FCM Token 已更新 你確定要傳送來自%1$s的%2$s? 你確定要傳送該%1$s? 分享二維碼 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8909430af1..b719247a3b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1057,6 +1057,8 @@ Share error. Share Link Share Logs + Update FCM Token + FCM token updated Are you sure you want to send a %2$s from %1$s? Are you sure you want to send the %1$s? Share QR Code From 3044414dd6b2d29745c320df6de381c2f3d8e6c0 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Wed, 24 Jun 2026 23:38:52 +0800 Subject: [PATCH 3/8] fix(fcm): skip worker retry without token --- .../main/java/one/mixin/android/worker/SessionWorker.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/one/mixin/android/worker/SessionWorker.kt b/app/src/main/java/one/mixin/android/worker/SessionWorker.kt index 0d0729a4cd..588db08769 100644 --- a/app/src/main/java/one/mixin/android/worker/SessionWorker.kt +++ b/app/src/main/java/one/mixin/android/worker/SessionWorker.kt @@ -38,15 +38,15 @@ class SessionWorker @AssistedInject constructor( val token = try { retrieveFirebaseToken() } catch (e: Exception) { - Timber.e(e, "Failed to retrieve Firebase token, retrying session update") + Timber.e(e, "Failed to retrieve Firebase token, skipping session update") reportException(IllegalStateException("SessionWorker failed to retrieve Firebase token", e)) - return Result.retry() + return Result.success() } if (token.isBlank()) { val error = IllegalStateException("SessionWorker retrieved blank Firebase token") - Timber.e(error, "Failed to retrieve Firebase token, retrying session update") + Timber.e(error, "Failed to retrieve Firebase token, skipping session update") reportException(error) - return Result.retry() + return Result.success() } Timber.e("Firebase token retrieved: true") From 0cdd859d901d2142455420c05d5fc1bc4e369b07 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Thu, 25 Jun 2026 14:20:52 +0800 Subject: [PATCH 4/8] fix(fcm): reset installation on auth errors Reset Firebase Installations on FIS_AUTH_ERROR before retrying token retrieval. Avoid reading Firebase installation id results before the task completes when building device ids. --- .../android/extension/ContextExtension.kt | 9 +++++- .../ui/setting/LogAndDebugViewModel.kt | 5 ++-- .../mixin/android/util/FirebaseTokenUtil.kt | 30 +++++++++++++++++++ .../one/mixin/android/worker/SessionWorker.kt | 5 ++-- 4 files changed, 42 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/one/mixin/android/util/FirebaseTokenUtil.kt diff --git a/app/src/main/java/one/mixin/android/extension/ContextExtension.kt b/app/src/main/java/one/mixin/android/extension/ContextExtension.kt index 35a0eb409b..8da191c7dc 100644 --- a/app/src/main/java/one/mixin/android/extension/ContextExtension.kt +++ b/app/src/main/java/one/mixin/android/extension/ContextExtension.kt @@ -1343,11 +1343,18 @@ fun Activity.showPipPermissionNotification( fun getStringDeviceId(resolver: ContentResolver): String { var deviceId = Settings.Secure.getString(resolver, Settings.Secure.ANDROID_ID) if (deviceId == null || deviceId == "9774d56d682e549c") { - deviceId = FirebaseInstallations.getInstance().id.result + deviceId = getFirebaseInstallationIdIfReady() ?: Build.FINGERPRINT } return UUID.nameUUIDFromBytes(deviceId.toByteArray()).toString() } +private fun getFirebaseInstallationIdIfReady(): String? { + val task = FirebaseInstallations.getInstance().id + if (!task.isComplete) return null + task.exception?.let { Timber.w(it, "Firebase installation id unavailable for device id") } + return if (task.isSuccessful) task.result else null +} + fun Context.getStringDeviceId(): String { return getStringDeviceId(contentResolver) } diff --git a/app/src/main/java/one/mixin/android/ui/setting/LogAndDebugViewModel.kt b/app/src/main/java/one/mixin/android/ui/setting/LogAndDebugViewModel.kt index bf1d949e54..b57763e893 100644 --- a/app/src/main/java/one/mixin/android/ui/setting/LogAndDebugViewModel.kt +++ b/app/src/main/java/one/mixin/android/ui/setting/LogAndDebugViewModel.kt @@ -1,13 +1,12 @@ package one.mixin.android.ui.setting import androidx.lifecycle.ViewModel -import com.google.firebase.messaging.FirebaseMessaging import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.tasks.await import one.mixin.android.api.request.SessionRequest import one.mixin.android.api.service.AccountService import one.mixin.android.repository.TokenRepository import one.mixin.android.session.Session +import one.mixin.android.util.retrieveFirebaseMessagingToken import timber.log.Timber import javax.inject.Inject @@ -37,7 +36,7 @@ class LogAndDebugViewModel @Inject constructor( } val token = try { - FirebaseMessaging.getInstance().token.await() + retrieveFirebaseMessagingToken() } catch (e: Exception) { Timber.e(e, "Debug FCM token retrieval failed") return FcmTokenUpdateResult.Failure("Failed to retrieve Firebase token: ${e.displayMessage()}") diff --git a/app/src/main/java/one/mixin/android/util/FirebaseTokenUtil.kt b/app/src/main/java/one/mixin/android/util/FirebaseTokenUtil.kt new file mode 100644 index 0000000000..cf8de09723 --- /dev/null +++ b/app/src/main/java/one/mixin/android/util/FirebaseTokenUtil.kt @@ -0,0 +1,30 @@ +package one.mixin.android.util + +import com.google.firebase.installations.FirebaseInstallations +import com.google.firebase.messaging.FirebaseMessaging +import kotlinx.coroutines.tasks.await +import timber.log.Timber + +private const val FIS_AUTH_ERROR = "FIS_AUTH_ERROR" + +suspend fun retrieveFirebaseMessagingToken(): String = + try { + FirebaseMessaging.getInstance().token.await() + } catch (e: Exception) { + if (!e.isFisAuthError()) throw e + Timber.w(e, "Firebase token retrieval failed with FIS_AUTH_ERROR") + try { + Timber.w("Deleting Firebase installation before retrying token retrieval") + FirebaseInstallations.getInstance().delete().await() + Timber.w("Firebase installation deleted, retrying token retrieval") + } catch (deleteError: Exception) { + Timber.e(deleteError, "Failed to delete Firebase installation after FIS_AUTH_ERROR") + throw deleteError + } + FirebaseMessaging.getInstance().token.await() + } + +private fun Throwable.isFisAuthError(): Boolean = + generateSequence(this) { it.cause }.any { throwable -> + throwable.message?.contains(FIS_AUTH_ERROR, ignoreCase = true) == true + } diff --git a/app/src/main/java/one/mixin/android/worker/SessionWorker.kt b/app/src/main/java/one/mixin/android/worker/SessionWorker.kt index 588db08769..57f7c4609d 100644 --- a/app/src/main/java/one/mixin/android/worker/SessionWorker.kt +++ b/app/src/main/java/one/mixin/android/worker/SessionWorker.kt @@ -3,16 +3,15 @@ package one.mixin.android.worker import android.content.Context import androidx.hilt.work.HiltWorker import androidx.work.WorkerParameters -import com.google.firebase.messaging.FirebaseMessaging import dagger.assisted.Assisted import dagger.assisted.AssistedInject -import kotlinx.coroutines.tasks.await import one.mixin.android.api.request.SessionRequest import one.mixin.android.api.service.AccountService import one.mixin.android.extension.isGooglePlayServicesAvailable import one.mixin.android.session.Session import one.mixin.android.util.ErrorHandler.Companion.SERVER import one.mixin.android.util.reportException +import one.mixin.android.util.retrieveFirebaseMessagingToken import timber.log.Timber @HiltWorker @@ -68,5 +67,5 @@ class SessionWorker @AssistedInject constructor( } } - private suspend fun retrieveFirebaseToken(): String = FirebaseMessaging.getInstance().token.await() + private suspend fun retrieveFirebaseToken(): String = retrieveFirebaseMessagingToken() } From 32fb3ec3c67ae0cd9712630a30fb4d9564ccd30a Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Thu, 25 Jun 2026 17:03:12 +0800 Subject: [PATCH 5/8] fix(fcm): post session without firebase token --- .../one/mixin/android/worker/SessionWorker.kt | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/one/mixin/android/worker/SessionWorker.kt b/app/src/main/java/one/mixin/android/worker/SessionWorker.kt index 57f7c4609d..45179810f0 100644 --- a/app/src/main/java/one/mixin/android/worker/SessionWorker.kt +++ b/app/src/main/java/one/mixin/android/worker/SessionWorker.kt @@ -7,7 +7,6 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import one.mixin.android.api.request.SessionRequest import one.mixin.android.api.service.AccountService -import one.mixin.android.extension.isGooglePlayServicesAvailable import one.mixin.android.session.Session import one.mixin.android.util.ErrorHandler.Companion.SERVER import one.mixin.android.util.reportException @@ -20,7 +19,6 @@ class SessionWorker @AssistedInject constructor( @Assisted parameters: WorkerParameters, private val accountService: AccountService, ) : BaseWork(context, parameters) { - private val appContext = context.applicationContext override suspend fun onRun(): Result { Timber.e("SessionWorker started") @@ -29,30 +27,28 @@ class SessionWorker @AssistedInject constructor( Timber.w("Session update failed: No active account") return Result.failure() } - if (!appContext.isGooglePlayServicesAvailable()) { - Timber.w("Session update skipped: Google Play services unavailable") - return Result.success() - } val token = try { retrieveFirebaseToken() } catch (e: Exception) { - Timber.e(e, "Failed to retrieve Firebase token, skipping session update") + Timber.e(e, "Failed to retrieve Firebase token") reportException(IllegalStateException("SessionWorker failed to retrieve Firebase token", e)) - return Result.success() + null } - if (token.isBlank()) { + val notificationToken = if (token != null && token.isBlank()) { val error = IllegalStateException("SessionWorker retrieved blank Firebase token") - Timber.e(error, "Failed to retrieve Firebase token, skipping session update") + Timber.e(error, "Failed to retrieve Firebase token") reportException(error) - return Result.success() + null + } else { + token } - Timber.e("Firebase token retrieved: true") + Timber.e("Firebase token retrieved: ${notificationToken != null}") return try { - val response = accountService.updateSession(SessionRequest(notificationToken = token)) + val response = accountService.updateSession(SessionRequest(notificationToken = notificationToken)) if (response.isSuccess) { - Timber.e("Session updated successfully with Firebase token") + Timber.e("Session updated successfully") Result.success() } else if (response.errorCode >= SERVER) { Timber.e("Session update failed with server error, retrying...") From c3dc43f17a3f01d436d5cfa5a995b99f812380da Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Thu, 25 Jun 2026 17:16:44 +0800 Subject: [PATCH 6/8] fix(fcm): use shared exception reporting --- app/src/main/java/one/mixin/android/worker/SessionWorker.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/one/mixin/android/worker/SessionWorker.kt b/app/src/main/java/one/mixin/android/worker/SessionWorker.kt index 45179810f0..fd06829556 100644 --- a/app/src/main/java/one/mixin/android/worker/SessionWorker.kt +++ b/app/src/main/java/one/mixin/android/worker/SessionWorker.kt @@ -32,7 +32,7 @@ class SessionWorker @AssistedInject constructor( retrieveFirebaseToken() } catch (e: Exception) { Timber.e(e, "Failed to retrieve Firebase token") - reportException(IllegalStateException("SessionWorker failed to retrieve Firebase token", e)) + reportException("SessionWorker failed to retrieve Firebase token", e) null } val notificationToken = if (token != null && token.isBlank()) { From 36627e6129d290fb1eb33334363a95fa4779d475 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Thu, 25 Jun 2026 17:25:08 +0800 Subject: [PATCH 7/8] fix(reporting): send exceptions to bugsnag --- .../java/one/mixin/android/util/CrashExceptionReport.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/one/mixin/android/util/CrashExceptionReport.kt b/app/src/main/java/one/mixin/android/util/CrashExceptionReport.kt index 9c6d130a9d..a84cd448f9 100644 --- a/app/src/main/java/one/mixin/android/util/CrashExceptionReport.kt +++ b/app/src/main/java/one/mixin/android/util/CrashExceptionReport.kt @@ -2,11 +2,13 @@ package one.mixin.android.util import androidx.media3.common.PlaybackException import androidx.media3.datasource.HttpDataSource +import com.bugsnag.android.Bugsnag import com.google.firebase.crashlytics.FirebaseCrashlytics import one.mixin.android.extension.getStackTraceString fun reportException(e: Throwable) { FirebaseCrashlytics.getInstance().recordException(e) + Bugsnag.notify(e) } fun reportException( @@ -14,6 +16,11 @@ fun reportException( e: Throwable, ) { FirebaseCrashlytics.getInstance().log(msg + e.getStackTraceString()) + FirebaseCrashlytics.getInstance().recordException(e) + Bugsnag.notify(e) { report -> + report.addError(e.javaClass.name, msg) + true + } } fun reportEvent(msg: String) { @@ -38,4 +45,4 @@ fun Throwable.msg(): String { return this.javaClass.name } return message ?: "Unknown" -} \ No newline at end of file +} From 5cef7d07c3aa0ac776dad9010f64c94275eeac89 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Thu, 25 Jun 2026 17:36:24 +0800 Subject: [PATCH 8/8] fix(fcm): report key errors to firebase and bugsnag --- .../ui/setting/LogAndDebugViewModel.kt | 4 ++++ .../android/util/CrashExceptionReport.kt | 8 +++++++- .../mixin/android/util/FirebaseTokenUtil.kt | 1 + .../one/mixin/android/worker/SessionWorker.kt | 20 ++++++++++++------- 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/one/mixin/android/ui/setting/LogAndDebugViewModel.kt b/app/src/main/java/one/mixin/android/ui/setting/LogAndDebugViewModel.kt index b57763e893..cea238af52 100644 --- a/app/src/main/java/one/mixin/android/ui/setting/LogAndDebugViewModel.kt +++ b/app/src/main/java/one/mixin/android/ui/setting/LogAndDebugViewModel.kt @@ -6,6 +6,7 @@ import one.mixin.android.api.request.SessionRequest import one.mixin.android.api.service.AccountService import one.mixin.android.repository.TokenRepository import one.mixin.android.session.Session +import one.mixin.android.util.reportFcmException import one.mixin.android.util.retrieveFirebaseMessagingToken import timber.log.Timber import javax.inject.Inject @@ -39,9 +40,12 @@ class LogAndDebugViewModel @Inject constructor( retrieveFirebaseMessagingToken() } catch (e: Exception) { Timber.e(e, "Debug FCM token retrieval failed") + reportFcmException("Debug FCM token retrieval failed", e) return FcmTokenUpdateResult.Failure("Failed to retrieve Firebase token: ${e.displayMessage()}") } if (token.isBlank()) { + val error = IllegalStateException("Debug FCM token is blank") + reportFcmException("Debug FCM token retrieval failed: blank token", error) Timber.e("Debug FCM token retrieval failed: blank token") return FcmTokenUpdateResult.Failure("Firebase token is blank") } diff --git a/app/src/main/java/one/mixin/android/util/CrashExceptionReport.kt b/app/src/main/java/one/mixin/android/util/CrashExceptionReport.kt index a84cd448f9..07953bfc92 100644 --- a/app/src/main/java/one/mixin/android/util/CrashExceptionReport.kt +++ b/app/src/main/java/one/mixin/android/util/CrashExceptionReport.kt @@ -8,12 +8,18 @@ import one.mixin.android.extension.getStackTraceString fun reportException(e: Throwable) { FirebaseCrashlytics.getInstance().recordException(e) - Bugsnag.notify(e) } fun reportException( msg: String, e: Throwable, +) { + FirebaseCrashlytics.getInstance().log(msg + e.getStackTraceString()) +} + +fun reportFcmException( + msg: String, + e: Throwable, ) { FirebaseCrashlytics.getInstance().log(msg + e.getStackTraceString()) FirebaseCrashlytics.getInstance().recordException(e) diff --git a/app/src/main/java/one/mixin/android/util/FirebaseTokenUtil.kt b/app/src/main/java/one/mixin/android/util/FirebaseTokenUtil.kt index cf8de09723..6264cbc896 100644 --- a/app/src/main/java/one/mixin/android/util/FirebaseTokenUtil.kt +++ b/app/src/main/java/one/mixin/android/util/FirebaseTokenUtil.kt @@ -19,6 +19,7 @@ suspend fun retrieveFirebaseMessagingToken(): String = Timber.w("Firebase installation deleted, retrying token retrieval") } catch (deleteError: Exception) { Timber.e(deleteError, "Failed to delete Firebase installation after FIS_AUTH_ERROR") + reportException("Failed to delete Firebase installation after FIS_AUTH_ERROR", deleteError) throw deleteError } FirebaseMessaging.getInstance().token.await() diff --git a/app/src/main/java/one/mixin/android/worker/SessionWorker.kt b/app/src/main/java/one/mixin/android/worker/SessionWorker.kt index fd06829556..635f50ff10 100644 --- a/app/src/main/java/one/mixin/android/worker/SessionWorker.kt +++ b/app/src/main/java/one/mixin/android/worker/SessionWorker.kt @@ -7,9 +7,10 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import one.mixin.android.api.request.SessionRequest import one.mixin.android.api.service.AccountService +import one.mixin.android.extension.isGooglePlayServicesAvailable import one.mixin.android.session.Session import one.mixin.android.util.ErrorHandler.Companion.SERVER -import one.mixin.android.util.reportException +import one.mixin.android.util.reportFcmException import one.mixin.android.util.retrieveFirebaseMessagingToken import timber.log.Timber @@ -28,17 +29,22 @@ class SessionWorker @AssistedInject constructor( return Result.failure() } - val token = try { - retrieveFirebaseToken() - } catch (e: Exception) { - Timber.e(e, "Failed to retrieve Firebase token") - reportException("SessionWorker failed to retrieve Firebase token", e) + val token = if (applicationContext.isGooglePlayServicesAvailable()) { + try { + retrieveFirebaseToken() + } catch (e: Exception) { + Timber.e(e, "Failed to retrieve Firebase token") + reportFcmException("SessionWorker failed to retrieve Firebase token", e) + null + } + } else { + Timber.w("Google Play services unavailable, skipping Firebase token retrieval") null } val notificationToken = if (token != null && token.isBlank()) { val error = IllegalStateException("SessionWorker retrieved blank Firebase token") Timber.e(error, "Failed to retrieve Firebase token") - reportException(error) + reportFcmException("SessionWorker retrieved blank Firebase token", error) null } else { token