move debug export to using URI (#2991)

pull/2995/head
DaneEvans 2025-09-06 15:17:04 +10:00 zatwierdzone przez GitHub
rodzic 82b6266f0e
commit 3a9e5ffbbe
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
2 zmienionych plików z 60 dodań i 48 usunięć

Wyświetl plik

@ -18,8 +18,10 @@
package com.geeksville.mesh.ui.debug
import android.content.Context
import android.os.Environment
import android.net.Uri
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
@ -88,8 +90,6 @@ import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileOutputStream
import java.io.OutputStreamWriter
import java.nio.charset.StandardCharsets
import java.text.SimpleDateFormat
@ -136,6 +136,14 @@ internal fun DebugScreen(viewModel: DebugViewModel = hiltViewModel()) {
listState.requestScrollToItem(searchState.allMatches[searchState.currentMatchIndex].logIndex)
}
}
// Prepare a document creator for exporting logs via SAF
val exportLogsLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { createdUri ->
if (createdUri != null) {
scope.launch { exportAllLogsToUri(context, createdUri, filteredLogs) }
}
}
Column(modifier = Modifier.fillMaxSize()) {
LazyColumn(modifier = Modifier.fillMaxSize(), state = listState) {
stickyHeader {
@ -149,7 +157,11 @@ internal fun DebugScreen(viewModel: DebugViewModel = hiltViewModel()) {
logs = logs,
filterMode = filterMode,
onFilterModeChange = { filterMode = it },
onExportLogs = { scope.launch { exportAllLogs(context, filteredLogs) } },
onExportLogs = {
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
val fileName = "meshtastic_debug_$timestamp.txt"
exportLogsLauncher.launch(fileName)
},
)
}
items(filteredLogs, key = { it.uuid }) { log ->
@ -338,57 +350,54 @@ fun DebugMenuActions(viewModel: DebugViewModel = hiltViewModel(), modifier: Modi
}
}
private suspend fun exportAllLogs(context: Context, logs: List<UiMeshLog>) = withContext(Dispatchers.IO) {
try {
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
val fileName = "meshtastic_debug_$timestamp.txt"
val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val logFile = File(downloadsDir, fileName)
OutputStreamWriter(FileOutputStream(logFile), StandardCharsets.UTF_8).use { writer ->
logs.forEach { log ->
writer.write("${log.formattedReceivedDate} [${log.messageType}]\n")
writer.write(log.logMessage)
if (!log.decodedPayload.isNullOrBlank()) {
writer.write("\n\nDecoded Payload:\n{")
writer.write("\n")
// Redact Decoded keys.
log.decodedPayload.lineSequence().forEach { line ->
var outputLine = line
val redacted = redactedKeys.firstOrNull { line.contains(it) }
if (redacted != null) {
val idx = line.indexOf(':')
if (idx != -1) {
outputLine = line.substring(0, idx + 1)
outputLine += "<redacted>"
private suspend fun exportAllLogsToUri(context: Context, targetUri: Uri, logs: List<UiMeshLog>) =
withContext(Dispatchers.IO) {
try {
context.contentResolver.openOutputStream(targetUri)?.use { os ->
OutputStreamWriter(os, StandardCharsets.UTF_8).use { writer ->
logs.forEach { log ->
writer.write("${log.formattedReceivedDate} [${log.messageType}]\n")
writer.write(log.logMessage)
if (!log.decodedPayload.isNullOrBlank()) {
writer.write("\n\nDecoded Payload:\n{")
writer.write("\n")
// Redact Decoded keys.
log.decodedPayload.lineSequence().forEach { line ->
var outputLine = line
val redacted = redactedKeys.firstOrNull { line.contains(it) }
if (redacted != null) {
val idx = line.indexOf(':')
if (idx != -1) {
outputLine = line.substring(0, idx + 1)
outputLine += "<redacted>"
}
}
writer.write(outputLine)
writer.write("\n")
}
writer.write("\n}")
}
writer.write(outputLine)
writer.write("\n")
writer.write("\n\n")
}
writer.write("\n}")
}
writer.write("\n\n")
}
}
} ?: run { throw IOException("Unable to open output stream for URI: $targetUri") }
withContext(Dispatchers.Main) {
Toast.makeText(context, "${logs.size} logs exported to ${logFile.absolutePath}", Toast.LENGTH_LONG)
.show()
withContext(Dispatchers.Main) {
Toast.makeText(context, context.getString(R.string.debug_export_success, logs.size), Toast.LENGTH_LONG)
.show()
}
} catch (e: IOException) {
withContext(Dispatchers.Main) {
Toast.makeText(
context,
context.getString(R.string.debug_export_failed, e.message ?: ""),
Toast.LENGTH_LONG,
)
.show()
}
warn("Error:IOException: " + e.toString())
}
} catch (e: SecurityException) {
withContext(Dispatchers.Main) {
Toast.makeText(context, "Permission denied: Cannot write to Downloads folder", Toast.LENGTH_LONG).show()
warn("Error:SecurityException: " + e.toString())
}
} catch (e: IOException) {
withContext(Dispatchers.Main) {
Toast.makeText(context, "Failed to write log file: ${e.message}", Toast.LENGTH_LONG).show()
}
warn("Error:IOException: " + e.toString())
}
}
@Composable
private fun DecodedPayloadBlock(

Wyświetl plik

@ -133,6 +133,9 @@
<string name="debug_panel">Debug Panel</string>
<string name="debug_decoded_payload">Decoded Payload:</string>
<string name="debug_logs_export">Export Logs</string>
<string name="debug_export_cancelled">Export canceled</string>
<string name="debug_export_success">%1$d logs exported</string>
<string name="debug_export_failed">Failed to write log file: %1$s</string>
<string name="debug_filters">Filters</string>
<string name="debug_active_filters">Active filters</string>
<string name="debug_default_search">Search in logs…</string>