manager: Add the sulog log viewer interface and functionality

This commit is contained in:
ShirkNeko
2025-10-22 18:19:16 +08:00
parent 3b966c536b
commit 15b9c4dbbf
5 changed files with 701 additions and 7 deletions

View File

@@ -0,0 +1,640 @@
package com.sukisu.ultra.ui.screen
import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
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 com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.component.*
import com.sukisu.ultra.ui.theme.CardConfig
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
import com.sukisu.ultra.ui.theme.getCardColors
import com.sukisu.ultra.ui.theme.getCardElevation
import com.sukisu.ultra.ui.util.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
private val SPACING_SMALL = 4.dp
private val SPACING_MEDIUM = 8.dp
private val SPACING_LARGE = 16.dp
data class LogEntry(
val timestamp: String,
val type: LogType,
val uid: String,
val comm: String,
val details: String,
val pid: String,
val rawLine: String
)
enum class LogType(val displayName: String, val color: Color) {
SU_GRANT("SU_GRANT", Color(0xFF4CAF50)),
SU_EXEC("SU_EXEC", Color(0xFF2196F3)),
PERM_CHECK("PERM_CHECK", Color(0xFFFF9800)),
MANAGER_OP("MANAGER_OP", Color(0xFF9C27B0)),
UNKNOWN("UNKNOWN", Color(0xFF757575))
}
@OptIn(ExperimentalMaterial3Api::class)
@Destination<RootGraph>
@Composable
fun LogViewerScreen(navigator: DestinationsNavigator) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val snackBarHost = LocalSnackbarHost.current
val context = LocalContext.current
val scope = rememberCoroutineScope()
var logEntries by remember { mutableStateOf<List<LogEntry>>(emptyList()) }
var isLoading by remember { mutableStateOf(false) }
var selectedLogFile by rememberSaveable { mutableStateOf("current") }
var filterType by rememberSaveable { mutableStateOf<LogType?>(null) }
var searchQuery by rememberSaveable { mutableStateOf("") }
var showSearchBar by rememberSaveable { mutableStateOf(false) }
val filteredEntries = remember(logEntries, filterType, searchQuery) {
logEntries.filter { entry ->
val matchesFilter = filterType == null || entry.type == filterType
val matchesSearch = searchQuery.isEmpty() ||
entry.comm.contains(searchQuery, ignoreCase = true) ||
entry.details.contains(searchQuery, ignoreCase = true) ||
entry.uid.contains(searchQuery, ignoreCase = true)
matchesFilter && matchesSearch
}
}
val loadingDialog = rememberLoadingDialog()
val confirmDialog = rememberConfirmDialog()
LaunchedEffect(selectedLogFile) {
loadLogs(selectedLogFile) { entries ->
logEntries = entries
}
}
Scaffold(
topBar = {
LogViewerTopBar(
scrollBehavior = scrollBehavior,
onBackClick = { navigator.navigateUp() },
showSearchBar = showSearchBar,
searchQuery = searchQuery,
onSearchQueryChange = { searchQuery = it },
onSearchToggle = { showSearchBar = !showSearchBar },
onClearLogs = {
scope.launch {
val result = confirmDialog.awaitConfirm(
title = context.getString(R.string.log_viewer_clear_logs),
content = context.getString(R.string.log_viewer_clear_logs_confirm)
)
if (result == ConfirmResult.Confirmed) {
loadingDialog.withLoading {
clearLogs(selectedLogFile)
loadLogs(selectedLogFile) { entries ->
logEntries = entries
}
}
snackBarHost.showSnackbar(context.getString(R.string.log_viewer_logs_cleared))
}
}
}
)
},
snackbarHost = { SnackbarHost(snackBarHost) },
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
) { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
.nestedScroll(scrollBehavior.nestedScrollConnection)
) {
// 控制面板
LogControlPanel(
selectedLogFile = selectedLogFile,
onLogFileSelected = { selectedLogFile = it },
filterType = filterType,
onFilterTypeSelected = { filterType = it },
logCount = filteredEntries.size,
totalCount = logEntries.size
)
// 日志列表
if (isLoading) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
} else if (filteredEntries.isEmpty()) {
EmptyLogState(
hasLogs = logEntries.isNotEmpty(),
onRefresh = {
scope.launch {
loadLogs(selectedLogFile) { entries ->
logEntries = entries
}
}
}
)
} else {
LogList(
entries = filteredEntries,
modifier = Modifier.fillMaxSize()
)
}
}
}
}
@Composable
private fun LogControlPanel(
selectedLogFile: String,
onLogFileSelected: (String) -> Unit,
filterType: LogType?,
onFilterTypeSelected: (LogType?) -> Unit,
logCount: Int,
totalCount: Int
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = SPACING_LARGE, vertical = SPACING_MEDIUM),
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerLow),
elevation = getCardElevation()
) {
Column(
modifier = Modifier.padding(SPACING_LARGE)
) {
// 文件选择
Text(
text = stringResource(R.string.log_viewer_select_file),
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(SPACING_MEDIUM))
Row(
horizontalArrangement = Arrangement.spacedBy(SPACING_MEDIUM)
) {
FilterChip(
onClick = { onLogFileSelected("current") },
label = { Text(stringResource(R.string.log_viewer_current_log)) },
selected = selectedLogFile == "current"
)
FilterChip(
onClick = { onLogFileSelected("old") },
label = { Text(stringResource(R.string.log_viewer_old_log)) },
selected = selectedLogFile == "old"
)
}
Spacer(modifier = Modifier.height(SPACING_LARGE))
// 类型过滤
Text(
text = stringResource(R.string.log_viewer_filter_type),
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(SPACING_MEDIUM))
LazyRow(
horizontalArrangement = Arrangement.spacedBy(SPACING_MEDIUM)
) {
item {
FilterChip(
onClick = { onFilterTypeSelected(null) },
label = { Text(stringResource(R.string.log_viewer_all_types)) },
selected = filterType == null
)
}
items(LogType.entries.toTypedArray()) { type ->
FilterChip(
onClick = { onFilterTypeSelected(if (filterType == type) null else type) },
label = { Text(type.displayName) },
selected = filterType == type,
leadingIcon = {
Box(
modifier = Modifier
.size(8.dp)
.background(type.color, RoundedCornerShape(4.dp))
)
}
)
}
}
Spacer(modifier = Modifier.height(SPACING_MEDIUM))
// 统计信息
Text(
text = stringResource(R.string.log_viewer_showing_entries, logCount, totalCount),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
@Composable
private fun LogList(
entries: List<LogEntry>,
modifier: Modifier = Modifier
) {
val listState = rememberLazyListState()
LazyColumn(
state = listState,
modifier = modifier,
contentPadding = PaddingValues(horizontal = SPACING_LARGE, vertical = SPACING_MEDIUM),
verticalArrangement = Arrangement.spacedBy(SPACING_SMALL)
) {
items(entries) { entry ->
LogEntryCard(entry = entry)
}
}
}
@Composable
private fun LogEntryCard(entry: LogEntry) {
var expanded by remember { mutableStateOf(false) }
Card(
modifier = Modifier
.fillMaxWidth()
.clickable { expanded = !expanded },
colors = getCardColors(MaterialTheme.colorScheme.surface),
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
) {
Column(
modifier = Modifier.padding(SPACING_MEDIUM)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(SPACING_MEDIUM)
) {
Box(
modifier = Modifier
.size(12.dp)
.background(entry.type.color, RoundedCornerShape(6.dp))
)
Text(
text = entry.type.displayName,
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold
)
}
Text(
text = entry.timestamp,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Spacer(modifier = Modifier.height(SPACING_SMALL))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "UID: ${entry.uid}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = "PID: ${entry.pid}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Text(
text = entry.comm,
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Medium,
maxLines = if (expanded) Int.MAX_VALUE else 1,
overflow = TextOverflow.Ellipsis
)
if (entry.details.isNotEmpty()) {
Spacer(modifier = Modifier.height(SPACING_SMALL))
Text(
text = entry.details,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = if (expanded) Int.MAX_VALUE else 2,
overflow = TextOverflow.Ellipsis
)
}
AnimatedVisibility(
visible = expanded,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
Column {
Spacer(modifier = Modifier.height(SPACING_MEDIUM))
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
Spacer(modifier = Modifier.height(SPACING_MEDIUM))
Text(
text = stringResource(R.string.log_viewer_raw_log),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(SPACING_SMALL))
Text(
text = entry.rawLine,
style = MaterialTheme.typography.bodySmall,
fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
}
@Composable
private fun EmptyLogState(
hasLogs: Boolean,
onRefresh: () -> Unit
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(SPACING_LARGE)
) {
Icon(
imageVector = if (hasLogs) Icons.Filled.FilterList else Icons.Filled.Description,
contentDescription = null,
modifier = Modifier.size(64.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = stringResource(
if (hasLogs) R.string.log_viewer_no_matching_logs
else R.string.log_viewer_no_logs
),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Button(onClick = onRefresh) {
Icon(
imageVector = Icons.Filled.Refresh,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
Spacer(modifier = Modifier.width(SPACING_MEDIUM))
Text(stringResource(R.string.log_viewer_refresh))
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun LogViewerTopBar(
scrollBehavior: TopAppBarScrollBehavior? = null,
onBackClick: () -> Unit,
showSearchBar: Boolean,
searchQuery: String,
onSearchQueryChange: (String) -> Unit,
onSearchToggle: () -> Unit,
onClearLogs: () -> Unit
) {
val colorScheme = MaterialTheme.colorScheme
val cardColor = if (CardConfig.isCustomBackgroundEnabled) {
colorScheme.surfaceContainerLow
} else {
colorScheme.background
}
Column {
TopAppBar(
title = {
Text(
text = stringResource(R.string.log_viewer_title),
style = MaterialTheme.typography.titleLarge
)
},
navigationIcon = {
IconButton(onClick = onBackClick) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.log_viewer_back)
)
}
},
actions = {
IconButton(onClick = onSearchToggle) {
Icon(
imageVector = if (showSearchBar) Icons.Filled.SearchOff else Icons.Filled.Search,
contentDescription = stringResource(R.string.log_viewer_search)
)
}
IconButton(onClick = onClearLogs) {
Icon(
imageVector = Icons.Filled.DeleteSweep,
contentDescription = stringResource(R.string.log_viewer_clear_logs)
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = cardColor.copy(alpha = cardAlpha),
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
),
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
scrollBehavior = scrollBehavior
)
AnimatedVisibility(
visible = showSearchBar,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
OutlinedTextField(
value = searchQuery,
onValueChange = onSearchQueryChange,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = SPACING_LARGE, vertical = SPACING_MEDIUM),
placeholder = { Text(stringResource(R.string.log_viewer_search_placeholder)) },
leadingIcon = {
Icon(
imageVector = Icons.Filled.Search,
contentDescription = null
)
},
trailingIcon = {
if (searchQuery.isNotEmpty()) {
IconButton(onClick = { onSearchQueryChange("") }) {
Icon(
imageVector = Icons.Filled.Clear,
contentDescription = stringResource(R.string.log_viewer_clear_search)
)
}
}
},
singleLine = true
)
}
}
}
private suspend fun loadLogs(
logFile: String,
onLoaded: (List<LogEntry>) -> Unit
) {
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, "cat $logPath 2>/dev/null || echo ''")
val entries = parseLogEntries(result)
withContext(Dispatchers.Main) {
onLoaded(entries)
}
} catch (_: Exception) {
withContext(Dispatchers.Main) {
onLoaded(emptyList())
}
}
}
}
private suspend fun clearLogs(logFile: String) {
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"
}
runCmd(shell, "echo '' > $logPath")
} catch (_: Exception) {
// 忽略错误
}
}
}
private fun parseLogEntries(logContent: String): List<LogEntry> {
if (logContent.isBlank()) return emptyList()
return logContent.lines()
.filter { it.isNotBlank() && it.startsWith("[") }
.mapNotNull { line ->
try {
parseLogLine(line)
} catch (_: Exception) {
null
}
}
.reversed() // 最新的日志在前面
}
private fun parseLogLine(line: String): LogEntry? {
// 解析格式: [timestamp] TYPE: UID=xxx COMM=xxx ...
val timestampRegex = """\[(.*?)]""".toRegex()
val timestampMatch = timestampRegex.find(line) ?: return null
val timestamp = timestampMatch.groupValues[1]
val afterTimestamp = line.substring(timestampMatch.range.last + 1).trim()
val parts = afterTimestamp.split(":")
if (parts.size < 2) return null
val typeStr = parts[0].trim()
val type = when (typeStr) {
"SU_GRANT" -> LogType.SU_GRANT
"SU_EXEC" -> LogType.SU_EXEC
"PERM_CHECK" -> LogType.PERM_CHECK
"MANAGER_OP" -> LogType.MANAGER_OP
else -> LogType.UNKNOWN
}
val details = parts[1].trim()
val uid: String = extractValue(details, "UID") ?: ""
val comm: String = extractValue(details, "COMM") ?: ""
val pid: String = extractValue(details, "PID") ?: ""
// 构建详细信息字符串
val detailsStr = when (type) {
LogType.SU_GRANT -> {
val method: String = extractValue(details, "METHOD") ?: ""
"Method: $method"
}
LogType.SU_EXEC -> {
val target: String = extractValue(details, "TARGET") ?: ""
val result: String = extractValue(details, "RESULT") ?: ""
"Target: $target, Result: $result"
}
LogType.PERM_CHECK -> {
val result: String = extractValue(details, "RESULT") ?: ""
"Result: $result"
}
LogType.MANAGER_OP -> {
val op: String = extractValue(details, "OP") ?: ""
val managerUid: String = extractValue(details, "MANAGER_UID") ?: ""
val targetUid: String = extractValue(details, "TARGET_UID") ?: ""
"Operation: $op, Manager UID: $managerUid, Target UID: $targetUid"
}
else -> details
}
return LogEntry(
timestamp = timestamp,
type = type,
uid = uid,
comm = comm,
details = detailsStr,
pid = pid,
rawLine = line
)
}
private fun extractValue(text: String, key: String): String? {
val regex = """$key=(\S+)""".toRegex()
return regex.find(text)?.groupValues?.get(1)
}

View File

@@ -36,6 +36,7 @@ import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
import com.ramcosta.composedestinations.generated.destinations.LogViewerScreenDestination
import com.ramcosta.composedestinations.generated.destinations.MoreSettingsScreenDestination import com.ramcosta.composedestinations.generated.destinations.MoreSettingsScreenDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.sukisu.ultra.BuildConfig import com.sukisu.ultra.BuildConfig
@@ -398,6 +399,18 @@ fun SettingScreen(navigator: DestinationsNavigator) {
} }
) )
// 查看使用日志
KsuIsValid {
SettingItem(
icon = Icons.Filled.Visibility,
title = stringResource(R.string.log_viewer_view_logs),
summary = stringResource(R.string.log_viewer_view_logs_summary),
onClick = {
navigator.navigate(LogViewerScreenDestination)
}
)
}
if (showBottomsheet) { if (showBottomsheet) {
LogBottomSheet( LogBottomSheet(
onDismiss = { showBottomsheet = false }, onDismiss = { showBottomsheet = false },

View File

@@ -58,9 +58,9 @@ object CardConfig {
if (isCustom) isCustomDimSet = true if (isCustom) isCustomDimSet = true
} }
fun updateShadow(enabled: Boolean, elevation: Dp = 4.dp) { fun updateShadow(enabled: Boolean, elevation: Dp = cardElevation) {
isShadowEnabled = enabled isShadowEnabled = enabled
cardElevation = if (enabled) elevation else 0.dp cardElevation = if (enabled) elevation else cardElevation
} }
fun updateBackground(enabled: Boolean) { fun updateBackground(enabled: Boolean) {
@@ -128,7 +128,7 @@ object CardConfig {
isUserLightModeEnabled = prefs.getBoolean(Keys.IS_USER_LIGHT_MODE_ENABLED, false) isUserLightModeEnabled = prefs.getBoolean(Keys.IS_USER_LIGHT_MODE_ENABLED, false)
// 应用阴影设置 // 应用阴影设置
updateShadow(isShadowEnabled, if (isShadowEnabled) 4.dp else 0.dp) updateShadow(isShadowEnabled, if (isShadowEnabled) cardElevation else 0.dp)
} }
@Deprecated("使用 updateShadow 替代", ReplaceWith("updateShadow(enabled)")) @Deprecated("使用 updateShadow 替代", ReplaceWith("updateShadow(enabled)"))
@@ -151,16 +151,16 @@ object CardStyleProvider {
fun getCardElevation() = CardDefaults.cardElevation( fun getCardElevation() = CardDefaults.cardElevation(
defaultElevation = CardConfig.cardElevation, defaultElevation = CardConfig.cardElevation,
pressedElevation = if (CardConfig.isShadowEnabled) { pressedElevation = if (CardConfig.isShadowEnabled) {
(CardConfig.cardElevation.value + 4).dp (CardConfig.cardElevation.value + 0).dp
} else 0.dp, } else 0.dp,
focusedElevation = if (CardConfig.isShadowEnabled) { focusedElevation = if (CardConfig.isShadowEnabled) {
(CardConfig.cardElevation.value + 6).dp (CardConfig.cardElevation.value + 0).dp
} else 0.dp, } else 0.dp,
hoveredElevation = if (CardConfig.isShadowEnabled) { hoveredElevation = if (CardConfig.isShadowEnabled) {
(CardConfig.cardElevation.value + 2).dp (CardConfig.cardElevation.value + 0).dp
} else 0.dp, } else 0.dp,
draggedElevation = if (CardConfig.isShadowEnabled) { draggedElevation = if (CardConfig.isShadowEnabled) {
(CardConfig.cardElevation.value + 8).dp (CardConfig.cardElevation.value + 0).dp
} else 0.dp, } else 0.dp,
disabledElevation = 0.dp disabledElevation = 0.dp
) )

View File

@@ -687,4 +687,24 @@
<string name="sus_maps_description_text">从 /proc/self/[maps|smaps|smaps_rollup|map_files|mem|pagemap] 中隐藏内存映射的真实文件路径。请注意:此功能不支持隐藏匿名内存映射,也无法隐藏由注入库本身产生的内联钩子或 PLT 钩子。</string> <string name="sus_maps_description_text">从 /proc/self/[maps|smaps|smaps_rollup|map_files|mem|pagemap] 中隐藏内存映射的真实文件路径。请注意:此功能不支持隐藏匿名内存映射,也无法隐藏由注入库本身产生的内联钩子或 PLT 钩子。</string>
<string name="sus_maps_warning">重要提示:对于具备完善注入检测机制的应用,此功能可能无法有效绕过检测。</string> <string name="sus_maps_warning">重要提示:对于具备完善注入检测机制的应用,此功能可能无法有效绕过检测。</string>
<string name="sus_maps_debug_info">首先通过 ps -enf 查找目标应用的 PID 和 UID然后检查 /proc/&lt;pid&gt;/maps 中的相关路径,并与 /proc/1/mountinfo 中的设备号进行比对以确保一致性。只有当设备号一致时,隐藏映射才能正常工作。</string> <string name="sus_maps_debug_info">首先通过 ps -enf 查找目标应用的 PID 和 UID然后检查 /proc/&lt;pid&gt;/maps 中的相关路径,并与 /proc/1/mountinfo 中的设备号进行比对以确保一致性。只有当设备号一致时,隐藏映射才能正常工作。</string>
<string name="log_viewer_title">日志查看器</string>
<string name="log_viewer_back">返回</string>
<string name="log_viewer_search">搜索</string>
<string name="log_viewer_clear_logs">清空日志</string>
<string name="log_viewer_clear_logs_confirm">确定要清空选中的日志文件吗?此操作无法撤销。</string>
<string name="log_viewer_logs_cleared">日志清空成功</string>
<string name="log_viewer_select_file">选择日志文件</string>
<string name="log_viewer_current_log">当前日志</string>
<string name="log_viewer_old_log">旧日志</string>
<string name="log_viewer_filter_type">按类型筛选</string>
<string name="log_viewer_all_types">所有类型</string>
<string name="log_viewer_showing_entries">显示 %1$d / %2$d 条记录</string>
<string name="log_viewer_no_logs">未找到日志</string>
<string name="log_viewer_no_matching_logs">未找到匹配的日志</string>
<string name="log_viewer_refresh">刷新</string>
<string name="log_viewer_raw_log">原始日志</string>
<string name="log_viewer_search_placeholder">按 UID、命令或详情搜索…</string>
<string name="log_viewer_clear_search">清除搜索</string>
<string name="log_viewer_view_logs">查看使用日志</string>
<string name="log_viewer_view_logs_summary">查看 KernelSU 超级用户访问日志</string>
</resources> </resources>

View File

@@ -695,4 +695,25 @@ Important Note:\n
<string name="sus_maps_description_text">Hide the real file paths of memory mappings from /proc/self/[maps|smaps|smaps_rollup|map_files|mem|pagemap]. Please note: This feature does not support hiding anonymous memory mappings, nor can it hide inline hooks or PLT hooks caused by the injected library itself.</string> <string name="sus_maps_description_text">Hide the real file paths of memory mappings from /proc/self/[maps|smaps|smaps_rollup|map_files|mem|pagemap]. Please note: This feature does not support hiding anonymous memory mappings, nor can it hide inline hooks or PLT hooks caused by the injected library itself.</string>
<string name="sus_maps_warning">Important Notice: For applications with well-implemented injection detection mechanisms, this feature may not effectively bypass detection.</string> <string name="sus_maps_warning">Important Notice: For applications with well-implemented injection detection mechanisms, this feature may not effectively bypass detection.</string>
<string name="sus_maps_debug_info">First, find the target application\'s PID and UID using ps -enf, then check the relevant paths in /proc/&lt;pid&gt;/maps and compare the device numbers with those in /proc/1/mountinfo to ensure consistency. Only when the device numbers match can the map hiding function work properly.</string> <string name="sus_maps_debug_info">First, find the target application\'s PID and UID using ps -enf, then check the relevant paths in /proc/&lt;pid&gt;/maps and compare the device numbers with those in /proc/1/mountinfo to ensure consistency. Only when the device numbers match can the map hiding function work properly.</string>
<!-- Log Viewer -->
<string name="log_viewer_title">Log Viewer</string>
<string name="log_viewer_back">Back</string>
<string name="log_viewer_search">Search</string>
<string name="log_viewer_clear_logs">Clear Logs</string>
<string name="log_viewer_clear_logs_confirm">Are you sure you want to clear the selected log file? This action cannot be undone.</string>
<string name="log_viewer_logs_cleared">Logs cleared successfully</string>
<string name="log_viewer_select_file">Select Log File</string>
<string name="log_viewer_current_log">Current Log</string>
<string name="log_viewer_old_log">Old Log</string>
<string name="log_viewer_filter_type">Filter by Type</string>
<string name="log_viewer_all_types">All Types</string>
<string name="log_viewer_showing_entries">Showing %1$d of %2$d entries</string>
<string name="log_viewer_no_logs">No logs found</string>
<string name="log_viewer_no_matching_logs">No matching logs found</string>
<string name="log_viewer_refresh">Refresh</string>
<string name="log_viewer_raw_log">Raw Log</string>
<string name="log_viewer_search_placeholder">Search by UID, command, or details…</string>
<string name="log_viewer_clear_search">Clear search</string>
<string name="log_viewer_view_logs">View Usage Logs</string>
<string name="log_viewer_view_logs_summary">View KernelSU superuser access logs</string>
</resources> </resources>