feat: Implement crash reporting system with CrashLogsScreen and integration in ProfileScreen
This commit is contained in:
@@ -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
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user