mirror of
https://github.com/topjohnwu/Magisk.git
synced 2026-07-03 14:08:39 +02:00
Rewrite Magisk log tab with itemized parsed entries
Parse logcat-format lines into structured MagiskLogEntry objects with timestamp, level, tag, PID/TID, and message. Display each entry as a card with a colored log level badge, tag, timestamp, and expandable message text instead of dumping raw text. Made-with: Cursor
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package com.topjohnwu.magisk.ui.log
|
package com.topjohnwu.magisk.ui.log
|
||||||
|
|
||||||
import androidx.compose.foundation.horizontalScroll
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
@@ -11,23 +12,30 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.topjohnwu.magisk.core.ktx.timeDateFormat
|
import com.topjohnwu.magisk.core.ktx.timeDateFormat
|
||||||
@@ -88,7 +96,7 @@ fun LogScreen(viewModel: LogViewModel) {
|
|||||||
nestedScrollConnection = scrollBehavior.nestedScrollConnection
|
nestedScrollConnection = scrollBehavior.nestedScrollConnection
|
||||||
)
|
)
|
||||||
1 -> MagiskLogTab(
|
1 -> MagiskLogTab(
|
||||||
log = uiState.magiskLog,
|
entries = uiState.magiskLogEntries,
|
||||||
onSave = { viewModel.saveMagiskLog() },
|
onSave = { viewModel.saveMagiskLog() },
|
||||||
onClear = { viewModel.clearMagiskLog() },
|
onClear = { viewModel.clearMagiskLog() },
|
||||||
nestedScrollConnection = scrollBehavior.nestedScrollConnection
|
nestedScrollConnection = scrollBehavior.nestedScrollConnection
|
||||||
@@ -202,9 +210,14 @@ private fun SuLogCard(log: SuLog) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MagiskLogTab(log: String, onSave: () -> Unit, onClear: () -> Unit, nestedScrollConnection: NestedScrollConnection) {
|
private fun MagiskLogTab(
|
||||||
|
entries: List<MagiskLogEntry>,
|
||||||
|
onSave: () -> Unit,
|
||||||
|
onClear: () -> Unit,
|
||||||
|
nestedScrollConnection: NestedScrollConnection
|
||||||
|
) {
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
if (log.isBlank()) {
|
if (entries.isEmpty()) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
@@ -218,22 +231,21 @@ private fun MagiskLogTab(log: String, onSave: () -> Unit, onClear: () -> Unit, n
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Box(
|
val listState = rememberLazyListState(initialFirstVisibleItemIndex = entries.size - 1)
|
||||||
|
LazyColumn(
|
||||||
|
state = listState,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.nestedScroll(nestedScrollConnection)
|
.nestedScroll(nestedScrollConnection)
|
||||||
.horizontalScroll(rememberScrollState())
|
.padding(horizontal = 12.dp),
|
||||||
.verticalScroll(rememberScrollState())
|
contentPadding = PaddingValues(bottom = 88.dp),
|
||||||
.padding(12.dp)
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
.padding(bottom = 76.dp)
|
|
||||||
) {
|
) {
|
||||||
Text(
|
item { Spacer(Modifier.height(4.dp)) }
|
||||||
text = log,
|
items(entries.size, key = { it }) { index ->
|
||||||
fontFamily = FontFamily.Monospace,
|
MagiskLogCard(entries[index])
|
||||||
fontSize = 12.sp,
|
}
|
||||||
lineHeight = 16.sp,
|
item { Spacer(Modifier.height(4.dp)) }
|
||||||
color = MiuixTheme.colorScheme.onSurface
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,3 +266,86 @@ private fun MagiskLogTab(log: String, onSave: () -> Unit, onClear: () -> Unit, n
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MagiskLogCard(entry: MagiskLogEntry) {
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { expanded = !expanded }
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(12.dp)) {
|
||||||
|
if (entry.isParsed) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
LogLevelBadge(entry.level)
|
||||||
|
Text(
|
||||||
|
text = entry.tag,
|
||||||
|
style = MiuixTheme.textStyles.body1,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
text = entry.timestamp,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(4.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = entry.message,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
lineHeight = 16.sp,
|
||||||
|
color = MiuixTheme.colorScheme.onSurface,
|
||||||
|
maxLines = if (expanded) Int.MAX_VALUE else 3,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun LogLevelBadge(level: Char) {
|
||||||
|
val (bg, fg) = when (level) {
|
||||||
|
'V' -> Color(0xFF9E9E9E) to Color.White
|
||||||
|
'D' -> Color(0xFF2196F3) to Color.White
|
||||||
|
'I' -> Color(0xFF4CAF50) to Color.White
|
||||||
|
'W' -> Color(0xFFFFC107) to Color.Black
|
||||||
|
'E' -> Color(0xFFF44336) to Color.White
|
||||||
|
'F' -> Color(0xFF9C27B0) to Color.White
|
||||||
|
else -> Color(0xFF757575) to Color.White
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(4.dp))
|
||||||
|
.background(bg)
|
||||||
|
.padding(horizontal = 5.dp, vertical = 1.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = level.toString(),
|
||||||
|
fontSize = 10.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
color = fg,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ class LogViewModel(
|
|||||||
data class UiState(
|
data class UiState(
|
||||||
val loading: Boolean = true,
|
val loading: Boolean = true,
|
||||||
val magiskLog: String = "",
|
val magiskLog: String = "",
|
||||||
|
val magiskLogEntries: List<MagiskLogEntry> = emptyList(),
|
||||||
val suLogs: List<SuLog> = emptyList(),
|
val suLogs: List<SuLog> = emptyList(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -42,9 +43,11 @@ class LogViewModel(
|
|||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
magiskLogRaw = repo.fetchMagiskLogs()
|
magiskLogRaw = repo.fetchMagiskLogs()
|
||||||
val suLogs = repo.fetchSuLogs()
|
val suLogs = repo.fetchSuLogs()
|
||||||
|
val entries = MagiskLogParser.parse(magiskLogRaw)
|
||||||
_uiState.update { it.copy(
|
_uiState.update { it.copy(
|
||||||
loading = false,
|
loading = false,
|
||||||
magiskLog = magiskLogRaw,
|
magiskLog = magiskLogRaw,
|
||||||
|
magiskLogEntries = entries,
|
||||||
suLogs = suLogs,
|
suLogs = suLogs,
|
||||||
) }
|
) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package com.topjohnwu.magisk.ui.log
|
||||||
|
|
||||||
|
data class MagiskLogEntry(
|
||||||
|
val timestamp: String = "",
|
||||||
|
val pid: Int = 0,
|
||||||
|
val tid: Int = 0,
|
||||||
|
val level: Char = 'I',
|
||||||
|
val tag: String = "",
|
||||||
|
val message: String = "",
|
||||||
|
val isParsed: Boolean = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
object MagiskLogParser {
|
||||||
|
|
||||||
|
// Logcat format: "MM-DD HH:MM:SS.mmm PID TID LEVEL TAG : message"
|
||||||
|
private val logcatRegex = Regex(
|
||||||
|
"""(\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d{3})\s+(\d+)\s+(\d+)\s+([VDIWEF])\s+(.+?)\s*:\s+(.*)"""
|
||||||
|
)
|
||||||
|
|
||||||
|
fun parse(raw: String): List<MagiskLogEntry> {
|
||||||
|
if (raw.isBlank()) return emptyList()
|
||||||
|
|
||||||
|
val lines = raw.lines()
|
||||||
|
val result = mutableListOf<MagiskLogEntry>()
|
||||||
|
|
||||||
|
for (line in lines) {
|
||||||
|
if (line.isBlank()) continue
|
||||||
|
|
||||||
|
val match = logcatRegex.find(line)
|
||||||
|
if (match != null) {
|
||||||
|
result.add(
|
||||||
|
MagiskLogEntry(
|
||||||
|
timestamp = match.groupValues[1],
|
||||||
|
pid = match.groupValues[2].toIntOrNull() ?: 0,
|
||||||
|
tid = match.groupValues[3].toIntOrNull() ?: 0,
|
||||||
|
level = match.groupValues[4].firstOrNull() ?: 'I',
|
||||||
|
tag = match.groupValues[5].trim(),
|
||||||
|
message = match.groupValues[6],
|
||||||
|
isParsed = true,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else if (result.isNotEmpty() && result.last().isParsed) {
|
||||||
|
// Continuation line — append to previous entry
|
||||||
|
val prev = result.last()
|
||||||
|
result[result.lastIndex] = prev.copy(
|
||||||
|
message = prev.message + "\n" + line.trimEnd()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
result.add(
|
||||||
|
MagiskLogEntry(message = line.trimEnd())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user