manager: Avoid page crashes caused by excessive data.
This commit is contained in:
@@ -49,6 +49,9 @@ private val SPACING_SMALL = 4.dp
|
|||||||
private val SPACING_MEDIUM = 8.dp
|
private val SPACING_MEDIUM = 8.dp
|
||||||
private val SPACING_LARGE = 16.dp
|
private val SPACING_LARGE = 16.dp
|
||||||
|
|
||||||
|
private const val PAGE_SIZE = 10000
|
||||||
|
private const val MAX_TOTAL_LOGS = 100000
|
||||||
|
|
||||||
data class LogEntry(
|
data class LogEntry(
|
||||||
val timestamp: String,
|
val timestamp: String,
|
||||||
val type: LogType,
|
val type: LogType,
|
||||||
@@ -59,6 +62,13 @@ data class LogEntry(
|
|||||||
val rawLine: String
|
val rawLine: String
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class LogPageInfo(
|
||||||
|
val currentPage: Int = 0,
|
||||||
|
val totalPages: Int = 0,
|
||||||
|
val totalLogs: Int = 0,
|
||||||
|
val hasMore: Boolean = false
|
||||||
|
)
|
||||||
|
|
||||||
enum class LogType(val displayName: String, val color: Color) {
|
enum class LogType(val displayName: String, val color: Color) {
|
||||||
SU_GRANT("SU_GRANT", Color(0xFF4CAF50)),
|
SU_GRANT("SU_GRANT", Color(0xFF4CAF50)),
|
||||||
SU_EXEC("SU_EXEC", Color(0xFF2196F3)),
|
SU_EXEC("SU_EXEC", Color(0xFF2196F3)),
|
||||||
@@ -107,6 +117,8 @@ fun LogViewerScreen(navigator: DestinationsNavigator) {
|
|||||||
var filterType by rememberSaveable { mutableStateOf<LogType?>(null) }
|
var filterType by rememberSaveable { mutableStateOf<LogType?>(null) }
|
||||||
var searchQuery by rememberSaveable { mutableStateOf("") }
|
var searchQuery by rememberSaveable { mutableStateOf("") }
|
||||||
var showSearchBar by rememberSaveable { mutableStateOf(false) }
|
var showSearchBar by rememberSaveable { mutableStateOf(false) }
|
||||||
|
var pageInfo by remember { mutableStateOf(LogPageInfo()) }
|
||||||
|
var lastLogFileHash by remember { mutableStateOf("") }
|
||||||
val currentUid = remember { myUid().toString() }
|
val currentUid = remember { myUid().toString() }
|
||||||
|
|
||||||
val initialExcluded = remember {
|
val initialExcluded = remember {
|
||||||
@@ -148,23 +160,58 @@ fun LogViewerScreen(navigator: DestinationsNavigator) {
|
|||||||
val loadingDialog = rememberLoadingDialog()
|
val loadingDialog = rememberLoadingDialog()
|
||||||
val confirmDialog = rememberConfirmDialog()
|
val confirmDialog = rememberConfirmDialog()
|
||||||
|
|
||||||
val onManualRefresh: () -> Unit = {
|
val loadPage: (Int, Boolean) -> Unit = { page, forceRefresh ->
|
||||||
scope.launch {
|
scope.launch {
|
||||||
loadLogs(selectedLogFile) { logEntries = it }
|
if (isLoading) return@launch
|
||||||
|
|
||||||
|
isLoading = true
|
||||||
|
try {
|
||||||
|
loadLogsWithPagination(
|
||||||
|
selectedLogFile,
|
||||||
|
page,
|
||||||
|
forceRefresh,
|
||||||
|
lastLogFileHash
|
||||||
|
) { entries, newPageInfo, newHash ->
|
||||||
|
logEntries = if (page == 0 || forceRefresh) {
|
||||||
|
entries
|
||||||
|
} else {
|
||||||
|
logEntries + entries
|
||||||
|
}
|
||||||
|
pageInfo = newPageInfo
|
||||||
|
lastLogFileHash = newHash
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val onManualRefresh: () -> Unit = {
|
||||||
|
loadPage(0, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
val loadNextPage: () -> Unit = {
|
||||||
|
if (pageInfo.hasMore && !isLoading) {
|
||||||
|
loadPage(pageInfo.currentPage + 1, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(selectedLogFile) {
|
LaunchedEffect(selectedLogFile) {
|
||||||
while (true) {
|
while (true) {
|
||||||
delay(3_000)
|
delay(5_000)
|
||||||
onManualRefresh()
|
if (!isLoading) {
|
||||||
|
scope.launch {
|
||||||
|
val hasNewLogs = checkForNewLogs(selectedLogFile, lastLogFileHash)
|
||||||
|
if (hasNewLogs) {
|
||||||
|
loadPage(0, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(selectedLogFile) {
|
LaunchedEffect(selectedLogFile) {
|
||||||
loadLogs(selectedLogFile) { entries ->
|
loadPage(0, true)
|
||||||
logEntries = entries
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@@ -186,9 +233,7 @@ fun LogViewerScreen(navigator: DestinationsNavigator) {
|
|||||||
if (result == ConfirmResult.Confirmed) {
|
if (result == ConfirmResult.Confirmed) {
|
||||||
loadingDialog.withLoading {
|
loadingDialog.withLoading {
|
||||||
clearLogs(selectedLogFile)
|
clearLogs(selectedLogFile)
|
||||||
loadLogs(selectedLogFile) { entries ->
|
loadPage(0, true)
|
||||||
logEntries = entries
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
snackBarHost.showSnackbar(context.getString(R.string.log_viewer_logs_cleared))
|
snackBarHost.showSnackbar(context.getString(R.string.log_viewer_logs_cleared))
|
||||||
}
|
}
|
||||||
@@ -207,11 +252,16 @@ fun LogViewerScreen(navigator: DestinationsNavigator) {
|
|||||||
// 控制面板
|
// 控制面板
|
||||||
LogControlPanel(
|
LogControlPanel(
|
||||||
selectedLogFile = selectedLogFile,
|
selectedLogFile = selectedLogFile,
|
||||||
onLogFileSelected = { selectedLogFile = it },
|
onLogFileSelected = {
|
||||||
|
selectedLogFile = it
|
||||||
|
pageInfo = LogPageInfo()
|
||||||
|
logEntries = emptyList()
|
||||||
|
},
|
||||||
filterType = filterType,
|
filterType = filterType,
|
||||||
onFilterTypeSelected = { filterType = it },
|
onFilterTypeSelected = { filterType = it },
|
||||||
logCount = filteredEntries.size,
|
logCount = filteredEntries.size,
|
||||||
totalCount = logEntries.size,
|
totalCount = logEntries.size,
|
||||||
|
pageInfo = pageInfo,
|
||||||
excludedSubTypes = excludedSubTypes,
|
excludedSubTypes = excludedSubTypes,
|
||||||
onExcludeToggle = { excl ->
|
onExcludeToggle = { excl ->
|
||||||
excludedSubTypes = if (excl in excludedSubTypes)
|
excludedSubTypes = if (excl in excludedSubTypes)
|
||||||
@@ -222,7 +272,7 @@ fun LogViewerScreen(navigator: DestinationsNavigator) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// 日志列表
|
// 日志列表
|
||||||
if (isLoading) {
|
if (isLoading && logEntries.isEmpty()) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
@@ -232,17 +282,14 @@ fun LogViewerScreen(navigator: DestinationsNavigator) {
|
|||||||
} else if (filteredEntries.isEmpty()) {
|
} else if (filteredEntries.isEmpty()) {
|
||||||
EmptyLogState(
|
EmptyLogState(
|
||||||
hasLogs = logEntries.isNotEmpty(),
|
hasLogs = logEntries.isNotEmpty(),
|
||||||
onRefresh = {
|
onRefresh = onManualRefresh
|
||||||
scope.launch {
|
|
||||||
loadLogs(selectedLogFile) { entries ->
|
|
||||||
logEntries = entries
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
LogList(
|
LogList(
|
||||||
entries = filteredEntries,
|
entries = filteredEntries,
|
||||||
|
pageInfo = pageInfo,
|
||||||
|
isLoading = isLoading,
|
||||||
|
onLoadMore = loadNextPage,
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -258,6 +305,7 @@ private fun LogControlPanel(
|
|||||||
onFilterTypeSelected: (LogType?) -> Unit,
|
onFilterTypeSelected: (LogType?) -> Unit,
|
||||||
logCount: Int,
|
logCount: Int,
|
||||||
totalCount: Int,
|
totalCount: Int,
|
||||||
|
pageInfo: LogPageInfo,
|
||||||
excludedSubTypes: Set<LogExclType>,
|
excludedSubTypes: Set<LogExclType>,
|
||||||
onExcludeToggle: (LogExclType) -> Unit
|
onExcludeToggle: (LogExclType) -> Unit
|
||||||
) {
|
) {
|
||||||
@@ -361,12 +409,37 @@ private fun LogControlPanel(
|
|||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(SPACING_MEDIUM))
|
Spacer(modifier = Modifier.height(SPACING_MEDIUM))
|
||||||
|
|
||||||
// 统计信息
|
// 统计信息和分页信息
|
||||||
Text(
|
Column(
|
||||||
text = stringResource(R.string.log_viewer_showing_entries, logCount, totalCount),
|
verticalArrangement = Arrangement.spacedBy(SPACING_SMALL)
|
||||||
style = MaterialTheme.typography.bodySmall,
|
) {
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
Text(
|
||||||
)
|
text = stringResource(R.string.log_viewer_showing_entries, logCount, totalCount),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
|
||||||
|
if (pageInfo.totalPages > 0) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
R.string.log_viewer_page_info,
|
||||||
|
pageInfo.currentPage + 1,
|
||||||
|
pageInfo.totalPages,
|
||||||
|
pageInfo.totalLogs
|
||||||
|
),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pageInfo.totalLogs >= MAX_TOTAL_LOGS) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.log_viewer_too_many_logs, MAX_TOTAL_LOGS),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -374,6 +447,9 @@ private fun LogControlPanel(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun LogList(
|
private fun LogList(
|
||||||
entries: List<LogEntry>,
|
entries: List<LogEntry>,
|
||||||
|
pageInfo: LogPageInfo,
|
||||||
|
isLoading: Boolean,
|
||||||
|
onLoadMore: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
@@ -387,6 +463,52 @@ private fun LogList(
|
|||||||
items(entries) { entry ->
|
items(entries) { entry ->
|
||||||
LogEntryCard(entry = entry)
|
LogEntryCard(entry = entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加载更多按钮或加载指示器
|
||||||
|
if (pageInfo.hasMore) {
|
||||||
|
item {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(SPACING_LARGE),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
if (isLoading) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Button(
|
||||||
|
onClick = onLoadMore,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.ExpandMore,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(SPACING_MEDIUM))
|
||||||
|
Text(stringResource(R.string.log_viewer_load_more))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (entries.isNotEmpty()) {
|
||||||
|
item {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(SPACING_LARGE),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.log_viewer_all_logs_loaded),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -632,9 +754,35 @@ private fun LogViewerTopBar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun loadLogs(
|
private suspend fun checkForNewLogs(
|
||||||
logFile: String,
|
logFile: String,
|
||||||
onLoaded: (List<LogEntry>) -> Unit
|
lastHash: String
|
||||||
|
): Boolean {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val shell = getRootShell()
|
||||||
|
val logPath = if (logFile == "current") {
|
||||||
|
"/data/adb/ksu/log/sulog.log"
|
||||||
|
} else {
|
||||||
|
"/data/adb/ksu/log/sulog.log.old"
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = runCmd(shell, "stat -c '%Y %s' $logPath 2>/dev/null || echo '0 0'")
|
||||||
|
val currentHash = result.trim()
|
||||||
|
|
||||||
|
currentHash != lastHash && currentHash != "0 0"
|
||||||
|
} catch (_: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadLogsWithPagination(
|
||||||
|
logFile: String,
|
||||||
|
page: Int,
|
||||||
|
forceRefresh: Boolean,
|
||||||
|
lastHash: String,
|
||||||
|
onLoaded: (List<LogEntry>, LogPageInfo, String) -> Unit
|
||||||
) {
|
) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
@@ -645,15 +793,62 @@ private suspend fun loadLogs(
|
|||||||
"/data/adb/ksu/log/sulog.log.old"
|
"/data/adb/ksu/log/sulog.log.old"
|
||||||
}
|
}
|
||||||
|
|
||||||
val result = runCmd(shell, "cat $logPath 2>/dev/null || echo ''")
|
// 获取文件信息
|
||||||
|
val statResult = runCmd(shell, "stat -c '%Y %s' $logPath 2>/dev/null || echo '0 0'")
|
||||||
|
val currentHash = statResult.trim()
|
||||||
|
|
||||||
|
// 如果不是强制刷新且文件没有变化,则不加载
|
||||||
|
if (!forceRefresh && currentHash == lastHash && currentHash != "0 0") {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
onLoaded(emptyList(), LogPageInfo(), currentHash)
|
||||||
|
}
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取总行数
|
||||||
|
val totalLinesResult = runCmd(shell, "wc -l < $logPath 2>/dev/null || echo '0'")
|
||||||
|
val totalLines = totalLinesResult.trim().toIntOrNull() ?: 0
|
||||||
|
|
||||||
|
if (totalLines == 0) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
onLoaded(emptyList(), LogPageInfo(), currentHash)
|
||||||
|
}
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
|
||||||
|
// 限制最大日志数量
|
||||||
|
val effectiveTotal = minOf(totalLines, MAX_TOTAL_LOGS)
|
||||||
|
val totalPages = (effectiveTotal + PAGE_SIZE - 1) / PAGE_SIZE
|
||||||
|
|
||||||
|
// 计算要读取的行数范围
|
||||||
|
val startLine = if (page == 0) {
|
||||||
|
maxOf(1, totalLines - effectiveTotal + 1)
|
||||||
|
} else {
|
||||||
|
val skipLines = page * PAGE_SIZE
|
||||||
|
maxOf(1, totalLines - effectiveTotal + 1 + skipLines)
|
||||||
|
}
|
||||||
|
|
||||||
|
val endLine = minOf(startLine + PAGE_SIZE - 1, totalLines)
|
||||||
|
|
||||||
|
if (startLine > totalLines) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
onLoaded(emptyList(), LogPageInfo(page, totalPages, effectiveTotal, false), currentHash)
|
||||||
|
}
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = runCmd(shell, "sed -n '${startLine},${endLine}p' $logPath 2>/dev/null || echo ''")
|
||||||
val entries = parseLogEntries(result)
|
val entries = parseLogEntries(result)
|
||||||
|
|
||||||
|
val hasMore = endLine < totalLines
|
||||||
|
val pageInfo = LogPageInfo(page, totalPages, effectiveTotal, hasMore)
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
onLoaded(entries)
|
onLoaded(entries, pageInfo, currentHash)
|
||||||
}
|
}
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
onLoaded(emptyList())
|
onLoaded(emptyList(), LogPageInfo(), lastHash)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -679,7 +874,7 @@ private suspend fun clearLogs(logFile: String) {
|
|||||||
private fun parseLogEntries(logContent: String): List<LogEntry> {
|
private fun parseLogEntries(logContent: String): List<LogEntry> {
|
||||||
if (logContent.isBlank()) return emptyList()
|
if (logContent.isBlank()) return emptyList()
|
||||||
|
|
||||||
return logContent.lines()
|
val entries = logContent.lines()
|
||||||
.filter { it.isNotBlank() && it.startsWith("[") }
|
.filter { it.isNotBlank() && it.startsWith("[") }
|
||||||
.mapNotNull { line ->
|
.mapNotNull { line ->
|
||||||
try {
|
try {
|
||||||
@@ -688,7 +883,8 @@ private fun parseLogEntries(logContent: String): List<LogEntry> {
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.reversed() // 最新的日志在前面
|
|
||||||
|
return entries.reversed()
|
||||||
}
|
}
|
||||||
private fun utcToLocal(utc: String): String {
|
private fun utcToLocal(utc: String): String {
|
||||||
return try {
|
return try {
|
||||||
|
|||||||
@@ -709,4 +709,8 @@
|
|||||||
<string name="log_viewer_view_logs_summary">查看 KernelSU 超级用户访问日志</string>
|
<string name="log_viewer_view_logs_summary">查看 KernelSU 超级用户访问日志</string>
|
||||||
<string name="log_viewer_exclude_subtypes">排除子类型</string>
|
<string name="log_viewer_exclude_subtypes">排除子类型</string>
|
||||||
<string name="log_viewer_exclude_current_app">当前应用</string>
|
<string name="log_viewer_exclude_current_app">当前应用</string>
|
||||||
|
<string name="log_viewer_page_info">页面: %1$d/%2$d | 总日志: %3$d</string>
|
||||||
|
<string name="log_viewer_too_many_logs">日志过多,仅显示最新 %1$d 条</string>
|
||||||
|
<string name="log_viewer_load_more">加载更多日志</string>
|
||||||
|
<string name="log_viewer_all_logs_loaded">已显示所有日志</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -718,4 +718,8 @@ Important Note:\n
|
|||||||
<string name="log_viewer_view_logs_summary">View KernelSU superuser access logs</string>
|
<string name="log_viewer_view_logs_summary">View KernelSU superuser access logs</string>
|
||||||
<string name="log_viewer_exclude_subtypes">Exclude sub-types</string>
|
<string name="log_viewer_exclude_subtypes">Exclude sub-types</string>
|
||||||
<string name="log_viewer_exclude_current_app">Current App</string>
|
<string name="log_viewer_exclude_current_app">Current App</string>
|
||||||
|
<string name="log_viewer_page_info">Page: %1$d/%2$d | Total logs: %3$d</string>
|
||||||
|
<string name="log_viewer_too_many_logs">Too many logs, showing only the latest %1$d entries</string>
|
||||||
|
<string name="log_viewer_load_more">Load More Logs</string>
|
||||||
|
<string name="log_viewer_all_logs_loaded">All logs displayed</string>
|
||||||
</resources>
|
</resources>
|
||||||
Reference in New Issue
Block a user