feat: Implement crash reporting system with CrashLogsScreen and integration in ProfileScreen
This commit is contained in:
@@ -0,0 +1,337 @@
|
||||
package com.rosetta.messenger.ui.crashlogs
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.BugReport
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.rosetta.messenger.utils.CrashReportManager
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Экран для просмотра crash logs
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun CrashLogsScreen(
|
||||
onBackClick: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var crashReports by remember { mutableStateOf<List<CrashReportManager.CrashReport>>(emptyList()) }
|
||||
var selectedReport by remember { mutableStateOf<CrashReportManager.CrashReport?>(null) }
|
||||
var showDeleteAllDialog by remember { mutableStateOf(false) }
|
||||
var showDeleteDialog by remember { mutableStateOf<String?>(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")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user