From 3a9e5ffbbe96572d6d85d8e354272eea0e1fa0db Mon Sep 17 00:00:00 2001 From: DaneEvans Date: Sat, 6 Sep 2025 15:17:04 +1000 Subject: [PATCH] move debug export to using URI (#2991) --- .../com/geeksville/mesh/ui/debug/Debug.kt | 105 ++++++++++-------- app/src/main/res/values/strings.xml | 3 + 2 files changed, 60 insertions(+), 48 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/ui/debug/Debug.kt b/app/src/main/java/com/geeksville/mesh/ui/debug/Debug.kt index 62c486d84..9ba603edf 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/debug/Debug.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/debug/Debug.kt @@ -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) = 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 += "" +private suspend fun exportAllLogsToUri(context: Context, targetUri: Uri, logs: List) = + 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 += "" + } + } + 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( diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 35ff4cd95..b4010469b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -133,6 +133,9 @@ Debug Panel Decoded Payload: Export Logs + Export canceled + %1$d logs exported + Failed to write log file: %1$s Filters Active filters Search in logs…