kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
move debug export to using URI (#2991)
rodzic
82b6266f0e
commit
3a9e5ffbbe
|
|
@ -18,8 +18,10 @@
|
||||||
package com.geeksville.mesh.ui.debug
|
package com.geeksville.mesh.ui.debug
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Environment
|
import android.net.Uri
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
|
@ -88,8 +90,6 @@ import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.io.OutputStreamWriter
|
import java.io.OutputStreamWriter
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
|
@ -136,6 +136,14 @@ internal fun DebugScreen(viewModel: DebugViewModel = hiltViewModel()) {
|
||||||
listState.requestScrollToItem(searchState.allMatches[searchState.currentMatchIndex].logIndex)
|
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()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
LazyColumn(modifier = Modifier.fillMaxSize(), state = listState) {
|
LazyColumn(modifier = Modifier.fillMaxSize(), state = listState) {
|
||||||
stickyHeader {
|
stickyHeader {
|
||||||
|
|
@ -149,7 +157,11 @@ internal fun DebugScreen(viewModel: DebugViewModel = hiltViewModel()) {
|
||||||
logs = logs,
|
logs = logs,
|
||||||
filterMode = filterMode,
|
filterMode = filterMode,
|
||||||
onFilterModeChange = { filterMode = it },
|
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 ->
|
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) {
|
private suspend fun exportAllLogsToUri(context: Context, targetUri: Uri, logs: List<UiMeshLog>) =
|
||||||
try {
|
withContext(Dispatchers.IO) {
|
||||||
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
|
try {
|
||||||
val fileName = "meshtastic_debug_$timestamp.txt"
|
context.contentResolver.openOutputStream(targetUri)?.use { os ->
|
||||||
|
OutputStreamWriter(os, StandardCharsets.UTF_8).use { writer ->
|
||||||
val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
logs.forEach { log ->
|
||||||
val logFile = File(downloadsDir, fileName)
|
writer.write("${log.formattedReceivedDate} [${log.messageType}]\n")
|
||||||
|
writer.write(log.logMessage)
|
||||||
OutputStreamWriter(FileOutputStream(logFile), StandardCharsets.UTF_8).use { writer ->
|
if (!log.decodedPayload.isNullOrBlank()) {
|
||||||
logs.forEach { log ->
|
writer.write("\n\nDecoded Payload:\n{")
|
||||||
writer.write("${log.formattedReceivedDate} [${log.messageType}]\n")
|
writer.write("\n")
|
||||||
writer.write(log.logMessage)
|
// Redact Decoded keys.
|
||||||
if (!log.decodedPayload.isNullOrBlank()) {
|
log.decodedPayload.lineSequence().forEach { line ->
|
||||||
writer.write("\n\nDecoded Payload:\n{")
|
var outputLine = line
|
||||||
writer.write("\n")
|
val redacted = redactedKeys.firstOrNull { line.contains(it) }
|
||||||
// Redact Decoded keys.
|
if (redacted != null) {
|
||||||
log.decodedPayload.lineSequence().forEach { line ->
|
val idx = line.indexOf(':')
|
||||||
var outputLine = line
|
if (idx != -1) {
|
||||||
val redacted = redactedKeys.firstOrNull { line.contains(it) }
|
outputLine = line.substring(0, idx + 1)
|
||||||
if (redacted != null) {
|
outputLine += "<redacted>"
|
||||||
val idx = line.indexOf(':')
|
}
|
||||||
if (idx != -1) {
|
}
|
||||||
outputLine = line.substring(0, idx + 1)
|
writer.write(outputLine)
|
||||||
outputLine += "<redacted>"
|
writer.write("\n")
|
||||||
}
|
}
|
||||||
|
writer.write("\n}")
|
||||||
}
|
}
|
||||||
writer.write(outputLine)
|
writer.write("\n\n")
|
||||||
writer.write("\n")
|
|
||||||
}
|
}
|
||||||
writer.write("\n}")
|
|
||||||
}
|
}
|
||||||
writer.write("\n\n")
|
} ?: run { throw IOException("Unable to open output stream for URI: $targetUri") }
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
Toast.makeText(context, "${logs.size} logs exported to ${logFile.absolutePath}", Toast.LENGTH_LONG)
|
Toast.makeText(context, context.getString(R.string.debug_export_success, logs.size), Toast.LENGTH_LONG)
|
||||||
.show()
|
.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
|
@Composable
|
||||||
private fun DecodedPayloadBlock(
|
private fun DecodedPayloadBlock(
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,9 @@
|
||||||
<string name="debug_panel">Debug Panel</string>
|
<string name="debug_panel">Debug Panel</string>
|
||||||
<string name="debug_decoded_payload">Decoded Payload:</string>
|
<string name="debug_decoded_payload">Decoded Payload:</string>
|
||||||
<string name="debug_logs_export">Export Logs</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_filters">Filters</string>
|
||||||
<string name="debug_active_filters">Active filters</string>
|
<string name="debug_active_filters">Active filters</string>
|
||||||
<string name="debug_default_search">Search in logs…</string>
|
<string name="debug_default_search">Search in logs…</string>
|
||||||
|
|
|
||||||
Ładowanie…
Reference in New Issue