feat: Implement crash reporting system with CrashLogsScreen and integration in ProfileScreen

This commit is contained in:
2026-01-25 02:33:56 +05:00
parent 766ab84f8c
commit c8214cdfa3
7 changed files with 878 additions and 1 deletions

View File

@@ -0,0 +1,215 @@
package com.rosetta.messenger.utils
import android.content.Context
import android.os.Build
import android.util.Log
import java.io.File
import java.io.PrintWriter
import java.io.StringWriter
import java.text.SimpleDateFormat
import java.util.*
/**
* Менеджер для сохранения crash reports в локальное хранилище
*/
class CrashReportManager private constructor(private val context: Context) : Thread.UncaughtExceptionHandler {
private val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
private val dateFormat = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.getDefault())
companion object {
private const val TAG = "CrashReportManager"
private const val CRASH_DIR = "crash_reports"
private const val MAX_CRASH_FILES = 50 // Максимум файлов с крашами
@Volatile
private var INSTANCE: CrashReportManager? = null
/**
* Инициализация crash reporter
*/
fun init(context: Context) {
if (INSTANCE == null) {
synchronized(this) {
if (INSTANCE == null) {
INSTANCE = CrashReportManager(context.applicationContext)
Thread.setDefaultUncaughtExceptionHandler(INSTANCE)
Log.d(TAG, "Crash reporter initialized")
}
}
}
}
/**
* Получить список всех crash reports
*/
fun getCrashReports(context: Context): List<CrashReport> {
val crashDir = File(context.filesDir, CRASH_DIR)
if (!crashDir.exists()) return emptyList()
return crashDir.listFiles()
?.filter { it.extension == "txt" }
?.sortedByDescending { it.lastModified() }
?.map { file ->
CrashReport(
fileName = file.name,
timestamp = file.lastModified(),
content = file.readText()
)
} ?: emptyList()
}
/**
* Удалить старые crash reports
*/
fun deleteCrashReport(context: Context, fileName: String): Boolean {
val crashDir = File(context.filesDir, CRASH_DIR)
val file = File(crashDir, fileName)
return if (file.exists()) {
file.delete()
} else {
false
}
}
/**
* Удалить все crash reports
*/
fun deleteAllCrashReports(context: Context) {
val crashDir = File(context.filesDir, CRASH_DIR)
if (crashDir.exists()) {
crashDir.listFiles()?.forEach { it.delete() }
}
}
}
override fun uncaughtException(thread: Thread, throwable: Throwable) {
try {
saveCrashReport(thread, throwable)
} catch (e: Exception) {
Log.e(TAG, "Error saving crash report", e)
}
// Вызываем дефолтный handler (чтобы система тоже обработала краш)
defaultHandler?.uncaughtException(thread, throwable)
}
/**
* Сохранить crash report в файл
*/
private fun saveCrashReport(thread: Thread, throwable: Throwable) {
val timestamp = dateFormat.format(Date())
val fileName = "crash_$timestamp.txt"
val crashDir = File(context.filesDir, CRASH_DIR)
if (!crashDir.exists()) {
crashDir.mkdirs()
}
val crashFile = File(crashDir, fileName)
val report = buildCrashReport(thread, throwable)
crashFile.writeText(report)
Log.d(TAG, "Crash report saved: $fileName")
// Удаляем старые файлы если их слишком много
cleanupOldCrashFiles(crashDir)
}
/**
* Собрать информацию о краше
*/
private fun buildCrashReport(thread: Thread, throwable: Throwable): String {
val report = StringBuilder()
// Заголовок
report.append("=== CRASH REPORT ===\n\n")
// Время краша
report.append("Timestamp: ${SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(Date())}\n\n")
// Информация о приложении
report.append("=== App Info ===\n")
report.append("Package: ${context.packageName}\n")
try {
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
report.append("Version: ${packageInfo.versionName} (${packageInfo.longVersionCode})\n")
} catch (e: Exception) {
report.append("Version: Unknown\n")
}
report.append("\n")
// Информация об устройстве
report.append("=== Device Info ===\n")
report.append("Manufacturer: ${Build.MANUFACTURER}\n")
report.append("Model: ${Build.MODEL}\n")
report.append("Android Version: ${Build.VERSION.RELEASE} (API ${Build.VERSION.SDK_INT})\n")
report.append("Device: ${Build.DEVICE}\n")
report.append("Board: ${Build.BOARD}\n")
report.append("\n")
// Информация о треде
report.append("=== Thread Info ===\n")
report.append("Thread: ${thread.name}\n")
report.append("Thread ID: ${thread.id}\n")
report.append("\n")
// Stack trace
report.append("=== Exception ===\n")
report.append("Exception Type: ${throwable.javaClass.name}\n")
report.append("Message: ${throwable.message ?: "No message"}\n\n")
// Полный stack trace
report.append("=== Stack Trace ===\n")
val stringWriter = StringWriter()
val printWriter = PrintWriter(stringWriter)
throwable.printStackTrace(printWriter)
report.append(stringWriter.toString())
// Cause (если есть)
var cause = throwable.cause
var causeLevel = 1
while (cause != null) {
report.append("\n=== Caused by (level $causeLevel) ===\n")
report.append("Exception Type: ${cause.javaClass.name}\n")
report.append("Message: ${cause.message ?: "No message"}\n\n")
val causeWriter = StringWriter()
val causePrintWriter = PrintWriter(causeWriter)
cause.printStackTrace(causePrintWriter)
report.append(causeWriter.toString())
cause = cause.cause
causeLevel++
// Предотвращаем бесконечный цикл
if (causeLevel > 10) break
}
return report.toString()
}
/**
* Удалить старые crash файлы если их слишком много
*/
private fun cleanupOldCrashFiles(crashDir: File) {
val files = crashDir.listFiles()?.sortedByDescending { it.lastModified() } ?: return
if (files.size > MAX_CRASH_FILES) {
files.drop(MAX_CRASH_FILES).forEach { file ->
file.delete()
Log.d(TAG, "Deleted old crash file: ${file.name}")
}
}
}
/**
* Data class для представления crash report
*/
data class CrashReport(
val fileName: String,
val timestamp: Long,
val content: String
)
}