diff --git a/CRASH_REPORTS_SYSTEM.md b/CRASH_REPORTS_SYSTEM.md
new file mode 100644
index 0000000..159b061
--- /dev/null
+++ b/CRASH_REPORTS_SYSTEM.md
@@ -0,0 +1,256 @@
+# Система Crash Reports для Rosetta Android
+
+## 📋 Обзор
+
+Реализована система автоматического сохранения crash reports в приложении Rosetta Android. Теперь при каждом краше приложения информация о нем будет сохраняться в локальное хранилище для последующего анализа.
+
+## 🎯 Функциональность
+
+### Автоматический сбор
+
+- **Отлов крашей**: Все необработанные исключения автоматически перехватываются
+- **Детальная информация**: Сохраняется полная информация о краше:
+ - Timestamp (дата и время)
+ - Информация о приложении (package, версия)
+ - Информация об устройстве (модель, Android версия)
+ - Thread информация
+ - Exception type и message
+ - Полный stack trace
+ - Вложенные causes (до 10 уровней)
+
+### Управление логами
+
+- **Просмотр**: Удобный UI для просмотра всех крашей
+- **Детали**: Подробный просмотр каждого краша с возможностью копирования
+- **Удаление**: Можно удалить отдельный краш или все разом
+- **Автоочистка**: Хранится максимум 50 последних крашей
+
+## 📁 Файловая структура
+
+```
+rosetta-android/app/src/main/java/com/rosetta/messenger/
+├── RosettaApplication.kt # Application класс с инициализацией
+├── utils/
+│ └── CrashReportManager.kt # Основной класс управления крашами
+└── ui/
+ └── crashlogs/
+ └── CrashLogsScreen.kt # UI для просмотра логов
+```
+
+## 🚀 Как использовать
+
+### 1. Просмотр Crash Logs
+
+В приложении:
+
+1. Откройте **Профиль** (Settings)
+2. Нажмите на **Crash Logs**
+3. Увидите список всех крашей
+4. Нажмите на краш для просмотра деталей
+
+### 2. Программный доступ
+
+```kotlin
+// Получить список всех крашей
+val crashes = CrashReportManager.getCrashReports(context)
+
+// Удалить конкретный краш
+CrashReportManager.deleteCrashReport(context, fileName)
+
+// Удалить все краши
+CrashReportManager.deleteAllCrashReports(context)
+```
+
+### 3. Тестирование системы
+
+Для проверки работы crash reporter можно добавить тестовый краш:
+
+```kotlin
+// В любом месте приложения
+Button(onClick = {
+ throw RuntimeException("Test crash for debugging")
+}) {
+ Text("Test Crash")
+}
+```
+
+## 📝 Формат Crash Report
+
+Пример сохраненного crash report:
+
+```
+=== CRASH REPORT ===
+
+Timestamp: 2026-01-25 14:30:45
+
+=== App Info ===
+Package: com.rosetta.messenger
+Version: 1.0.0 (1)
+
+=== Device Info ===
+Manufacturer: Samsung
+Model: SM-G991B
+Android Version: 13 (API 33)
+Device: o1s
+Board: s5e9925
+
+=== Thread Info ===
+Thread: main
+Thread ID: 2
+
+=== Exception ===
+Exception Type: java.lang.NullPointerException
+Message: Attempt to invoke virtual method on null object
+
+=== Stack Trace ===
+java.lang.NullPointerException: Attempt to invoke virtual method on null object
+ at com.rosetta.messenger.ui.MainActivity.onCreate(MainActivity.kt:123)
+ at android.app.Activity.performCreate(Activity.java:8051)
+ ...
+
+=== Caused by (level 1) ===
+...
+```
+
+## 🔧 Настройки
+
+В [CrashReportManager.kt](rosetta-android/app/src/main/java/com/rosetta/messenger/utils/CrashReportManager.kt):
+
+```kotlin
+private const val MAX_CRASH_FILES = 50 // Максимум файлов
+private const val CRASH_DIR = "crash_reports" // Директория хранения
+```
+
+## 💾 Хранение данных
+
+- **Расположение**: `/data/data/com.rosetta.messenger/files/crash_reports/`
+- **Формат файлов**: `crash_YYYY-MM-DD_HH-mm-ss.txt`
+- **Автоочистка**: Старые файлы удаляются при превышении лимита
+
+## 🎨 UI Features
+
+### Список крашей
+
+- ❌ Красная иконка bug для каждого краша
+- 📅 Дата и время краша
+- 🔍 Тип исключения
+- 🗑️ Кнопка удаления для каждого
+
+### Детальный просмотр
+
+- 📄 Полный текст crash report
+- ✂️ Возможность выделения и копирования текста
+- 🔙 Навигация назад
+- 🗑️ Удаление краша
+
+### Пустое состояние
+
+Если нет крашей, показывается дружелюбное сообщение:
+
+```
+🐛 No crash reports
+Great! Your app is running smoothly
+```
+
+## 🔒 Безопасность и Privacy
+
+- ✅ Данные хранятся только локально
+- ✅ Не отправляются на сторонние серверы
+- ✅ Пользователь контролирует удаление
+- ✅ Автоматическая ротация старых логов
+
+## 🛠️ Техническая реализация
+
+### Инициализация
+
+Crash reporter автоматически инициализируется в `RosettaApplication.onCreate()`:
+
+```kotlin
+class RosettaApplication : Application() {
+ override fun onCreate() {
+ super.onCreate()
+ CrashReportManager.init(this)
+ }
+}
+```
+
+### Обработка крашей
+
+`CrashReportManager` реализует `Thread.UncaughtExceptionHandler`:
+
+- Перехватывает необработанные исключения
+- Сохраняет детальную информацию
+- Передает управление дефолтному handler (для нормального завершения)
+
+## 📊 Мониторинг
+
+### Что отслеживать:
+
+1. **Частота крашей**: Как часто происходят краши
+2. **Тип исключений**: Какие типы ошибок встречаются
+3. **Места крашей**: В каких частях кода происходят проблемы
+4. **Устройства**: На каких устройствах чаще крашится
+
+### Аналитика
+
+Регулярно проверяйте Crash Logs раздел для:
+
+- Выявления паттернов
+- Приоритизации багов
+- Улучшения стабильности
+
+## 🚧 Дальнейшие улучшения
+
+Возможные доработки:
+
+1. **Export**: Экспорт крашей в файл для отправки разработчикам
+2. **Filtering**: Фильтрация по типу исключения, дате
+3. **Statistics**: Статистика по типам крашей
+4. **Auto-report**: Опциональная отправка на сервер (с согласия)
+5. **Symbols**: Интеграция с символами для более читаемых stack traces
+
+## 🐛 Debug режим
+
+В debug режиме рекомендуется добавить быстрый доступ к Crash Logs:
+
+```kotlin
+// В ProfileScreen или DebugMenu
+if (BuildConfig.DEBUG) {
+ Button(onClick = {
+ throw RuntimeException("Test crash")
+ }) {
+ Text("Trigger Test Crash")
+ }
+}
+```
+
+## ✅ Чеклист интеграции
+
+- [x] CrashReportManager создан
+- [x] RosettaApplication настроен
+- [x] AndroidManifest обновлен
+- [x] UI для просмотра создан
+- [x] Навигация добавлена
+- [x] Автоочистка настроена
+
+## 📞 Получение крашей от пользователей
+
+Если пользователь сообщает о краше:
+
+1. Попросите открыть Profile → Crash Logs
+2. Найти краш по дате/времени
+3. Скопировать текст краша
+4. Отправить разработчикам
+
+## 🎯 Best Practices
+
+1. **Регулярная проверка**: Просматривайте краши минимум раз в неделю
+2. **Приоритеты**: Сначала исправляйте частые краши
+3. **Тестирование**: После исправления проверяйте что краш не воспроизводится
+4. **Документация**: Документируйте причины и решения крашей
+
+---
+
+**Статус**: ✅ Готово к использованию
+**Версия**: 1.0
+**Дата**: 25 января 2026
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index b508ec8..691f05f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -8,6 +8,7 @@
Unit
+) {
+ val context = LocalContext.current
+ var crashReports by remember { mutableStateOf>(emptyList()) }
+ var selectedReport by remember { mutableStateOf(null) }
+ var showDeleteAllDialog by remember { mutableStateOf(false) }
+ var showDeleteDialog by remember { mutableStateOf(null) }
+
+ // Загружаем crash reports
+ LaunchedEffect(Unit) {
+ crashReports = CrashReportManager.getCrashReports(context)
+ }
+
+ // Функция для обновления списка
+ fun refreshReports() {
+ crashReports = CrashReportManager.getCrashReports(context)
+ }
+
+ if (selectedReport != null) {
+ // Показываем детали краша
+ CrashDetailScreen(
+ crashReport = selectedReport!!,
+ onBackClick = { selectedReport = null },
+ onDelete = {
+ CrashReportManager.deleteCrashReport(context, selectedReport!!.fileName)
+ refreshReports()
+ selectedReport = null
+ }
+ )
+ } else {
+ // Список крашей
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text("Crash Logs") },
+ navigationIcon = {
+ IconButton(onClick = onBackClick) {
+ Icon(Icons.Default.ArrowBack, contentDescription = "Back")
+ }
+ },
+ actions = {
+ if (crashReports.isNotEmpty()) {
+ IconButton(onClick = { showDeleteAllDialog = true }) {
+ Icon(Icons.Default.Delete, contentDescription = "Delete All")
+ }
+ }
+ }
+ )
+ }
+ ) { paddingValues ->
+ if (crashReports.isEmpty()) {
+ // Пустое состояние
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues),
+ contentAlignment = Alignment.Center
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Icon(
+ imageVector = Icons.Default.BugReport,
+ contentDescription = null,
+ modifier = Modifier.size(64.dp),
+ tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.5f)
+ )
+ Text(
+ text = "No crash reports",
+ style = MaterialTheme.typography.bodyLarge,
+ color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
+ )
+ Text(
+ text = "Great! Your app is running smoothly",
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f)
+ )
+ }
+ }
+ } else {
+ // Список crash reports
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues),
+ contentPadding = PaddingValues(16.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ items(crashReports) { report ->
+ CrashReportItem(
+ crashReport = report,
+ onClick = { selectedReport = report },
+ onDelete = { showDeleteDialog = report.fileName }
+ )
+ }
+ }
+ }
+ }
+
+ // Диалог удаления всех
+ if (showDeleteAllDialog) {
+ AlertDialog(
+ onDismissRequest = { showDeleteAllDialog = false },
+ title = { Text("Delete All Crash Reports?") },
+ text = { Text("This will permanently delete all ${crashReports.size} crash reports.") },
+ confirmButton = {
+ TextButton(
+ onClick = {
+ CrashReportManager.deleteAllCrashReports(context)
+ refreshReports()
+ showDeleteAllDialog = false
+ }
+ ) {
+ Text("Delete All")
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = { showDeleteAllDialog = false }) {
+ Text("Cancel")
+ }
+ }
+ )
+ }
+
+ // Диалог удаления одного
+ if (showDeleteDialog != null) {
+ AlertDialog(
+ onDismissRequest = { showDeleteDialog = null },
+ title = { Text("Delete Crash Report?") },
+ text = { Text("This will permanently delete this crash report.") },
+ confirmButton = {
+ TextButton(
+ onClick = {
+ CrashReportManager.deleteCrashReport(context, showDeleteDialog!!)
+ refreshReports()
+ showDeleteDialog = null
+ }
+ ) {
+ Text("Delete")
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = { showDeleteDialog = null }) {
+ Text("Cancel")
+ }
+ }
+ )
+ }
+ }
+}
+
+/**
+ * Элемент списка с crash report
+ */
+@Composable
+private fun CrashReportItem(
+ crashReport: CrashReportManager.CrashReport,
+ onClick: () -> Unit,
+ onDelete: () -> Unit
+) {
+ val dateFormat = remember { SimpleDateFormat("dd MMM yyyy, HH:mm:ss", Locale.getDefault()) }
+ val exceptionType = remember {
+ crashReport.content.lines()
+ .find { it.startsWith("Exception Type:") }
+ ?.substringAfter("Exception Type: ")
+ ?.substringAfterLast(".")
+ ?: "Unknown"
+ }
+
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable(onClick = onClick),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.3f)
+ )
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Column(
+ modifier = Modifier.weight(1f),
+ verticalArrangement = Arrangement.spacedBy(4.dp)
+ ) {
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ imageVector = Icons.Default.BugReport,
+ contentDescription = null,
+ modifier = Modifier.size(20.dp),
+ tint = MaterialTheme.colorScheme.error
+ )
+ Text(
+ text = exceptionType,
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold
+ )
+ }
+ Text(
+ text = dateFormat.format(Date(crashReport.timestamp)),
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
+ )
+ }
+
+ IconButton(onClick = onDelete) {
+ Icon(
+ imageVector = Icons.Default.Delete,
+ contentDescription = "Delete",
+ tint = MaterialTheme.colorScheme.error
+ )
+ }
+ }
+ }
+}
+
+/**
+ * Детальный просмотр crash report
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun CrashDetailScreen(
+ crashReport: CrashReportManager.CrashReport,
+ onBackClick: () -> Unit,
+ onDelete: () -> Unit
+) {
+ var showDeleteDialog by remember { mutableStateOf(false) }
+
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text("Crash Details") },
+ navigationIcon = {
+ IconButton(onClick = onBackClick) {
+ Icon(Icons.Default.ArrowBack, contentDescription = "Back")
+ }
+ },
+ actions = {
+ IconButton(onClick = { /* TODO: Share */ }) {
+ Icon(Icons.Default.Share, contentDescription = "Share")
+ }
+ IconButton(onClick = { showDeleteDialog = true }) {
+ Icon(Icons.Default.Delete, contentDescription = "Delete")
+ }
+ }
+ )
+ }
+ ) { paddingValues ->
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues)
+ .background(MaterialTheme.colorScheme.surface),
+ contentPadding = PaddingValues(16.dp)
+ ) {
+ item {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surfaceVariant
+ )
+ ) {
+ Text(
+ text = crashReport.content,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ fontFamily = FontFamily.Monospace,
+ fontSize = 12.sp,
+ lineHeight = 18.sp
+ )
+ }
+ }
+ }
+
+ if (showDeleteDialog) {
+ AlertDialog(
+ onDismissRequest = { showDeleteDialog = false },
+ title = { Text("Delete Crash Report?") },
+ text = { Text("This will permanently delete this crash report.") },
+ confirmButton = {
+ TextButton(
+ onClick = {
+ showDeleteDialog = false
+ onDelete()
+ }
+ ) {
+ Text("Delete")
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = { showDeleteDialog = false }) {
+ Text("Cancel")
+ }
+ }
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/com/rosetta/messenger/ui/settings/ProfileScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/settings/ProfileScreen.kt
index 5355622..a16b76d 100644
--- a/app/src/main/java/com/rosetta/messenger/ui/settings/ProfileScreen.kt
+++ b/app/src/main/java/com/rosetta/messenger/ui/settings/ProfileScreen.kt
@@ -148,6 +148,7 @@ fun ProfileScreen(
onNavigateToTheme: () -> Unit = {},
onNavigateToSafety: () -> Unit = {},
onNavigateToLogs: () -> Unit = {},
+ onNavigateToCrashLogs: () -> Unit = {},
viewModel: ProfileViewModel = androidx.lifecycle.viewmodel.compose.viewModel(),
avatarRepository: AvatarRepository? = null,
dialogDao: com.rosetta.messenger.database.DialogDao? = null
@@ -425,6 +426,14 @@ fun ProfileScreen(
title = "Safety",
onClick = onNavigateToSafety,
isDarkTheme = isDarkTheme,
+ showDivider = true
+ )
+
+ TelegramSettingsItem(
+ icon = TablerIcons.Bug,
+ title = "Crash Logs",
+ onClick = onNavigateToCrashLogs,
+ isDarkTheme = isDarkTheme,
showDivider = biometricAvailable is BiometricAvailability.Available
)
diff --git a/app/src/main/java/com/rosetta/messenger/utils/CrashReportManager.kt b/app/src/main/java/com/rosetta/messenger/utils/CrashReportManager.kt
new file mode 100644
index 0000000..38be667
--- /dev/null
+++ b/app/src/main/java/com/rosetta/messenger/utils/CrashReportManager.kt
@@ -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 {
+ 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
+ )
+}