diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/LogViewerScreen.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/LogViewerScreen.kt new file mode 100644 index 00000000..33e9e4af --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/LogViewerScreen.kt @@ -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 +@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>(emptyList()) } + var isLoading by remember { mutableStateOf(false) } + var selectedLogFile by rememberSaveable { mutableStateOf("current") } + var filterType by rememberSaveable { mutableStateOf(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, + 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) -> 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 { + 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) +} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt index 1d640ea4..a84a2458 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt @@ -36,6 +36,7 @@ import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination 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.navigation.DestinationsNavigator 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) { LogBottomSheet( onDismiss = { showBottomsheet = false }, diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/theme/CardManage.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/theme/CardManage.kt index c827d9ef..27578522 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/theme/CardManage.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/theme/CardManage.kt @@ -58,9 +58,9 @@ object CardConfig { if (isCustom) isCustomDimSet = true } - fun updateShadow(enabled: Boolean, elevation: Dp = 4.dp) { + fun updateShadow(enabled: Boolean, elevation: Dp = cardElevation) { isShadowEnabled = enabled - cardElevation = if (enabled) elevation else 0.dp + cardElevation = if (enabled) elevation else cardElevation } fun updateBackground(enabled: Boolean) { @@ -128,7 +128,7 @@ object CardConfig { 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)")) @@ -151,16 +151,16 @@ object CardStyleProvider { fun getCardElevation() = CardDefaults.cardElevation( defaultElevation = CardConfig.cardElevation, pressedElevation = if (CardConfig.isShadowEnabled) { - (CardConfig.cardElevation.value + 4).dp + (CardConfig.cardElevation.value + 0).dp } else 0.dp, focusedElevation = if (CardConfig.isShadowEnabled) { - (CardConfig.cardElevation.value + 6).dp + (CardConfig.cardElevation.value + 0).dp } else 0.dp, hoveredElevation = if (CardConfig.isShadowEnabled) { - (CardConfig.cardElevation.value + 2).dp + (CardConfig.cardElevation.value + 0).dp } else 0.dp, draggedElevation = if (CardConfig.isShadowEnabled) { - (CardConfig.cardElevation.value + 8).dp + (CardConfig.cardElevation.value + 0).dp } else 0.dp, disabledElevation = 0.dp ) diff --git a/manager/app/src/main/res/values-zh-rCN/strings.xml b/manager/app/src/main/res/values-zh-rCN/strings.xml index 35fb29f4..08d125b8 100644 --- a/manager/app/src/main/res/values-zh-rCN/strings.xml +++ b/manager/app/src/main/res/values-zh-rCN/strings.xml @@ -687,4 +687,24 @@ 从 /proc/self/[maps|smaps|smaps_rollup|map_files|mem|pagemap] 中隐藏内存映射的真实文件路径。请注意:此功能不支持隐藏匿名内存映射,也无法隐藏由注入库本身产生的内联钩子或 PLT 钩子。 重要提示:对于具备完善注入检测机制的应用,此功能可能无法有效绕过检测。 首先通过 ps -enf 查找目标应用的 PID 和 UID,然后检查 /proc/<pid>/maps 中的相关路径,并与 /proc/1/mountinfo 中的设备号进行比对以确保一致性。只有当设备号一致时,隐藏映射才能正常工作。 + 日志查看器 + 返回 + 搜索 + 清空日志 + 确定要清空选中的日志文件吗?此操作无法撤销。 + 日志清空成功 + 选择日志文件 + 当前日志 + 旧日志 + 按类型筛选 + 所有类型 + 显示 %1$d / %2$d 条记录 + 未找到日志 + 未找到匹配的日志 + 刷新 + 原始日志 + 按 UID、命令或详情搜索… + 清除搜索 + 查看使用日志 + 查看 KernelSU 超级用户访问日志 diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml index 8991e3cd..2eaf3eef 100644 --- a/manager/app/src/main/res/values/strings.xml +++ b/manager/app/src/main/res/values/strings.xml @@ -695,4 +695,25 @@ Important Note:\n 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. Important Notice: For applications with well-implemented injection detection mechanisms, this feature may not effectively bypass detection. First, find the target application\'s PID and UID using ps -enf, then check the relevant paths in /proc/<pid>/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. + + Log Viewer + Back + Search + Clear Logs + Are you sure you want to clear the selected log file? This action cannot be undone. + Logs cleared successfully + Select Log File + Current Log + Old Log + Filter by Type + All Types + Showing %1$d of %2$d entries + No logs found + No matching logs found + Refresh + Raw Log + Search by UID, command, or details… + Clear search + View Usage Logs + View KernelSU superuser access logs \ No newline at end of file