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
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -100,6 +101,9 @@ class LogAndDebugFragment : BaseFragment(R.layout.fragment_log_debug) {
logs.setOnClickListener {
shareLogsFile()
}
updateFcmToken.setOnClickListener {
updateFcmToken()
}
database.setOnClickListener {
navTo(
DatabaseDebugFragment.newInstance(),
Expand Down Expand Up @@ -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")
}
Comment thread
SeniorZhai marked this conversation as resolved.
} 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,19 @@ package one.mixin.android.ui.setting

import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
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

@HiltViewModel
class LogAndDebugViewModel @Inject constructor(
private val tokenRepository: TokenRepository
private val tokenRepository: TokenRepository,
private val accountService: AccountService,
) : ViewModel() {

suspend fun deleteAllWeb3Transactions() {
Expand All @@ -21,4 +28,56 @@ 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")
}
Comment thread
SeniorZhai marked this conversation as resolved.

val token = try {
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")
}
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
}
15 changes: 14 additions & 1 deletion app/src/main/java/one/mixin/android/util/CrashExceptionReport.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ 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

Expand All @@ -16,6 +17,18 @@ fun reportException(
FirebaseCrashlytics.getInstance().log(msg + e.getStackTraceString())
}

fun reportFcmException(
msg: String,
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) {
FirebaseCrashlytics.getInstance().log(msg)
}
Expand All @@ -38,4 +51,4 @@ fun Throwable.msg(): String {
return this.javaClass.name
}
return message ?: "Unknown"
}
}
31 changes: 31 additions & 0 deletions app/src/main/java/one/mixin/android/util/FirebaseTokenUtil.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
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")
reportException("Failed to delete Firebase installation after FIS_AUTH_ERROR", deleteError)
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
}
40 changes: 27 additions & 13 deletions app/src/main/java/one/mixin/android/worker/SessionWorker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ 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 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.reportFcmException
import one.mixin.android.util.retrieveFirebaseMessagingToken
import timber.log.Timber

@HiltWorker
Expand All @@ -28,11 +29,30 @@ class SessionWorker @AssistedInject constructor(
return Result.failure()
}

val token = retrieveFirebaseToken()
Timber.e("Firebase token retrieved: ${token != null}")
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")
reportFcmException("SessionWorker retrieved blank Firebase token", error)
null
} else {
token
}
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")
Result.success()
Expand All @@ -49,11 +69,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)
}
}
private suspend fun retrieveFirebaseToken(): String = retrieveFirebaseMessagingToken()
}
12 changes: 12 additions & 0 deletions app/src/main/res/layout/fragment_log_debug.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,18 @@
android:foreground="?android:attr/selectableItemBackground"
android:textSize="16sp" />

<TextView
android:id="@+id/update_fcm_token"
android:layout_width="match_parent"
android:layout_height="@dimen/bottom_button_height"
android:gravity="center_vertical"
android:paddingStart="@dimen/activity_horizontal_margin"
android:paddingEnd="@dimen/activity_horizontal_margin"
android:text="@string/Update_FCM_Token"
android:textColor="@color/colorBlue"
android:foreground="?android:attr/selectableItemBackground"
android:textSize="16sp" />


<RelativeLayout
android:id="@+id/web_debug"
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values-zh-rCN/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1035,6 +1035,8 @@
<string name="Share_error">分享出错</string>
<string name="Share_Link">分享邀请链接</string>
<string name="Share_Logs">分享日志文件</string>
<string name="Update_FCM_Token">更新 FCM Token</string>
<string name="FCM_Token_Updated">FCM Token 已更新</string>
<string name="share_message_description">你确定要发送来自%1$s的%2$s?</string>
<string name="share_message_description_empty">你确定要发送该%1$s?</string>
<string name="Share_QR_Code">分享二维码</string>
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values-zh-rTW/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -838,6 +838,8 @@
<string name="Share_Contact">分享名片</string>
<string name="Share_error">分享出錯</string>
<string name="Share_Link">分享邀請連結</string>
<string name="Update_FCM_Token">更新 FCM Token</string>
<string name="FCM_Token_Updated">FCM Token 已更新</string>
<string name="share_message_description">你確定要傳送來自%1$s的%2$s?</string>
<string name="share_message_description_empty">你確定要傳送該%1$s?</string>
<string name="Share_QR_Code">分享二維碼</string>
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1057,6 +1057,8 @@
<string name="Share_error">Share error.</string>
<string name="Share_Link">Share Link</string>
<string name="Share_Logs">Share Logs</string>
<string name="Update_FCM_Token">Update FCM Token</string>
<string name="FCM_Token_Updated">FCM token updated</string>
<string name="share_message_description">Are you sure you want to send a %2$s from %1$s?</string>
<string name="share_message_description_empty">Are you sure you want to send the %1$s?</string>
<string name="Share_QR_Code">Share QR Code</string>
Expand Down