mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-11 00:12:24 +01:00
logs: share a log from an earlier launch
Opening Gearslip after a crash starts a new log, so the one that saw the crash could only be pulled with a computer. Logs now has an Earlier button listing past launches, newest first, marked when they crashed; tapping one shares it. The newest 30 are kept.
This commit is contained in:
1 parent
325e115752
commit
86a1f968ec
2 files changed
+111
-6
No files matched your search
@@ -52,6 +52,46 @@ object GearslipLog {
|
|||||||
file = File(dir, "gearslip-$name.log")
|
file = File(dir, "gearslip-$name.log")
|
||||||
i("log file: ${file?.absolutePath}")
|
i("log file: ${file?.absolutePath}")
|
||||||
SessionReport.init(context)
|
SessionReport.init(context)
|
||||||
|
// One file per launch adds up; the newest are the ones anyone asks for.
|
||||||
|
io.execute { logFiles(dir).drop(KEEP_FILES).forEach { it.delete() } }
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val KEEP_FILES = 30
|
||||||
|
|
||||||
|
private fun logFiles(dir: File): List<File> =
|
||||||
|
dir.listFiles { f -> f.isFile && f.name.startsWith("gearslip-") && f.name.endsWith(".log") }
|
||||||
|
?.sortedByDescending { it.name }.orEmpty()
|
||||||
|
|
||||||
|
/** A log from an earlier launch: when it started, and what a glance at it tells. */
|
||||||
|
class PastLog(val file: File, val started: Date?, val crashed: Boolean, val car: String?)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Earlier launches' logs, newest first. Each launch starts a fresh file, so after a crash the
|
||||||
|
* one that saw it is here rather than on the live screen.
|
||||||
|
*/
|
||||||
|
fun pastLogs(context: Context): List<PastLog> {
|
||||||
|
val dir = context.getExternalFilesDir(null) ?: return emptyList()
|
||||||
|
val parse = SimpleDateFormat("yyyyMMdd-HHmmss", Locale.US)
|
||||||
|
return logFiles(dir).filter { it != file }.map { f ->
|
||||||
|
val text = runCatching { f.readText() }.getOrDefault("")
|
||||||
|
PastLog(
|
||||||
|
file = f,
|
||||||
|
started = runCatching { parse.parse(f.name.removePrefix("gearslip-").removeSuffix(".log")) }.getOrNull(),
|
||||||
|
crashed = "FATAL uncaught" in text,
|
||||||
|
car = Regex("""car="([^"]+)"""").findAll(text).lastOrNull()?.groupValues?.get(1),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** An earlier launch's log, copied to the cache dir for sharing like [shareableFile]. */
|
||||||
|
fun shareableFile(context: Context, past: File): android.net.Uri {
|
||||||
|
val dir = File(context.cacheDir, "shared").apply { mkdirs() }
|
||||||
|
dir.listFiles()?.forEach { it.delete() }
|
||||||
|
val out = File(dir, "gearslip-log-${past.name.removePrefix("gearslip-").removeSuffix(".log")}.txt")
|
||||||
|
past.copyTo(out, overwrite = true)
|
||||||
|
return androidx.core.content.FileProvider.getUriForFile(
|
||||||
|
context, "${context.packageName}.logshare", out,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun snapshot(): List<String> = synchronized(live) { live.toList() }
|
fun snapshot(): List<String> = synchronized(live) { live.toList() }
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.navigationBarsPadding
|
|||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
@@ -55,6 +56,9 @@ fun LogsScreen(onBack: () -> Unit) {
|
|||||||
val version by GearslipLog.version.collectAsStateWithLifecycle()
|
val version by GearslipLog.version.collectAsStateWithLifecycle()
|
||||||
val lines = remember(version) { GearslipLog.snapshot() }
|
val lines = remember(version) { GearslipLog.snapshot() }
|
||||||
|
|
||||||
|
var pastOpen by remember { mutableStateOf(false) }
|
||||||
|
if (pastOpen) PastLogsDialog(onDismiss = { pastOpen = false })
|
||||||
|
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
val atEnd by remember {
|
val atEnd by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
@@ -97,16 +101,15 @@ fun LogsScreen(onBack: () -> Unit) {
|
|||||||
onClick = {
|
onClick = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val uri = withContext(Dispatchers.IO) { GearslipLog.shareableFile(context) }
|
val uri = withContext(Dispatchers.IO) { GearslipLog.shareableFile(context) }
|
||||||
val send = Intent(Intent.ACTION_SEND)
|
shareLog(context, uri)
|
||||||
.setType("text/plain")
|
|
||||||
.putExtra(Intent.EXTRA_SUBJECT, "Gearslip log")
|
|
||||||
.putExtra(Intent.EXTRA_STREAM, uri)
|
|
||||||
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
context.startActivity(Intent.createChooser(send, "Share log"))
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier.weight(1f).height(56.dp),
|
modifier = Modifier.weight(1f).height(56.dp),
|
||||||
) { Text("Share") }
|
) { Text("Share") }
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = { pastOpen = true },
|
||||||
|
modifier = Modifier.weight(1f).height(56.dp),
|
||||||
|
) { Text("Earlier") }
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onClick = { GearslipLog.clearLive() },
|
onClick = { GearslipLog.clearLive() },
|
||||||
modifier = Modifier.weight(1f).height(56.dp),
|
modifier = Modifier.weight(1f).height(56.dp),
|
||||||
@@ -153,6 +156,68 @@ fun LogsScreen(onBack: () -> Unit) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun shareLog(context: android.content.Context, uri: android.net.Uri) {
|
||||||
|
val send = Intent(Intent.ACTION_SEND)
|
||||||
|
.setType("text/plain")
|
||||||
|
.putExtra(Intent.EXTRA_SUBJECT, "Gearslip log")
|
||||||
|
.putExtra(Intent.EXTRA_STREAM, uri)
|
||||||
|
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
context.startActivity(Intent.createChooser(send, "Share log"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs from earlier launches. Opening the app after a crash starts a new log, so the one that
|
||||||
|
* saw the crash is only reachable here.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun PastLogsDialog(onDismiss: () -> Unit) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val logs by androidx.compose.runtime.produceState<List<GearslipLog.PastLog>?>(null) {
|
||||||
|
value = withContext(Dispatchers.IO) { GearslipLog.pastLogs(context) }
|
||||||
|
}
|
||||||
|
val format = remember { java.text.SimpleDateFormat("EEE d MMM, h:mm a", java.util.Locale.getDefault()) }
|
||||||
|
androidx.compose.material3.AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
title = { Text("Earlier logs") },
|
||||||
|
text = {
|
||||||
|
val list = logs
|
||||||
|
when {
|
||||||
|
list == null -> Text("Loading…")
|
||||||
|
list.isEmpty() -> Text("No earlier logs yet. Each time Gearslip starts, it begins a new one.")
|
||||||
|
else -> LazyColumn {
|
||||||
|
itemsIndexed(list) { _, log ->
|
||||||
|
androidx.compose.foundation.layout.Column(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
|
scope.launch {
|
||||||
|
val uri = withContext(Dispatchers.IO) { GearslipLog.shareableFile(context, log.file) }
|
||||||
|
shareLog(context, uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(vertical = 12.dp),
|
||||||
|
) {
|
||||||
|
Text(log.started?.let(format::format) ?: log.file.name, style = MaterialTheme.typography.titleMedium)
|
||||||
|
val detail = listOfNotNull(
|
||||||
|
"Crashed".takeIf { log.crashed },
|
||||||
|
log.car,
|
||||||
|
"${(log.file.length() + 1023) / 1024} KB",
|
||||||
|
).joinToString(" · ")
|
||||||
|
Text(
|
||||||
|
detail,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = if (log.crashed) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = { androidx.compose.material3.TextButton(onClick = onDismiss) { Text("Close") } },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/** Lines look like "HH:mm:ss.SSS L message"; the level letter sits at index 13. */
|
/** Lines look like "HH:mm:ss.SSS L message"; the level letter sits at index 13. */
|
||||||
@Composable
|
@Composable
|
||||||
private fun colorFor(line: String): Color = when (line.getOrNull(13)) {
|
private fun colorFor(line: String): Color = when (line.getOrNull(13)) {
|
||||||
|
|||||||
Reference in new issue
Block a user