Step 1: Import susfs and sulog to modify
This commit is contained in:
@@ -19,11 +19,38 @@ object Natives {
|
|||||||
// 12143: breaking: new supercall impl
|
// 12143: breaking: new supercall impl
|
||||||
const val MINIMAL_SUPPORTED_KERNEL = 22000
|
const val MINIMAL_SUPPORTED_KERNEL = 22000
|
||||||
|
|
||||||
|
// Get full version
|
||||||
|
external fun getFullVersion(): String
|
||||||
|
const val MINIMAL_SUPPORTED_KERNEL_FULL = "v4.0.0"
|
||||||
|
|
||||||
|
// 12040: Support disable sucompat mode
|
||||||
const val KERNEL_SU_DOMAIN = "u:r:su:s0"
|
const val KERNEL_SU_DOMAIN = "u:r:su:s0"
|
||||||
|
|
||||||
const val ROOT_UID = 0
|
const val ROOT_UID = 0
|
||||||
const val ROOT_GID = 0
|
const val ROOT_GID = 0
|
||||||
|
|
||||||
|
fun isVersionLessThan(v1Full: String, v2Full: String): Boolean {
|
||||||
|
fun extractVersionParts(version: String): List<Int> {
|
||||||
|
val match = Regex("""v\d+(\.\d+)*""").find(version)
|
||||||
|
val simpleVersion = match?.value ?: version
|
||||||
|
return simpleVersion.trimStart('v').split('.').map { it.toIntOrNull() ?: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
val v1Parts = extractVersionParts(v1Full)
|
||||||
|
val v2Parts = extractVersionParts(v2Full)
|
||||||
|
val maxLength = maxOf(v1Parts.size, v2Parts.size)
|
||||||
|
for (i in 0 until maxLength) {
|
||||||
|
val num1 = v1Parts.getOrElse(i) { 0 }
|
||||||
|
val num2 = v2Parts.getOrElse(i) { 0 }
|
||||||
|
if (num1 != num2) return num1 < num2
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSimpleVersionFull(): String = getFullVersion().let { version ->
|
||||||
|
Regex("""v\d+(\.\d+)*""").find(version)?.value ?: version
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
System.loadLibrary("kernelsu")
|
System.loadLibrary("kernelsu")
|
||||||
}
|
}
|
||||||
@@ -86,6 +113,67 @@ object Natives {
|
|||||||
*/
|
*/
|
||||||
external fun getUserName(uid: Int): String?
|
external fun getUserName(uid: Int): String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Su Log can be enabled/disabled.
|
||||||
|
* 0: disabled
|
||||||
|
* 1: enabled
|
||||||
|
* negative : error
|
||||||
|
*/
|
||||||
|
external fun isSuLogEnabled(): Boolean
|
||||||
|
external fun setSuLogEnabled(enabled: Boolean): Boolean
|
||||||
|
|
||||||
|
external fun isKPMEnabled(): Boolean
|
||||||
|
external fun getHookType(): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set dynamic managerature configuration
|
||||||
|
* @param size APK signature size
|
||||||
|
* @param hash APK signature hash (64 character hex string)
|
||||||
|
* @return true if successful, false otherwise
|
||||||
|
*/
|
||||||
|
external fun setDynamicManager(size: Int, hash: String): Boolean
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current dynamic managerature configuration
|
||||||
|
* @return DynamicManagerConfig object containing current configuration, or null if not set
|
||||||
|
*/
|
||||||
|
external fun getDynamicManager(): DynamicManagerConfig?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear dynamic managerature configuration
|
||||||
|
* @return true if successful, false otherwise
|
||||||
|
*/
|
||||||
|
external fun clearDynamicManager(): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get active managers list when dynamic manager is enabled
|
||||||
|
* @return ManagersList object containing active managers, or null if failed or not enabled
|
||||||
|
*/
|
||||||
|
external fun getManagersList(): ManagersList?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if UID scanner is currently enabled
|
||||||
|
* @return true if UID scanner is enabled, false otherwise
|
||||||
|
*/
|
||||||
|
external fun isUidScannerEnabled(): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable or disable UID scanner
|
||||||
|
* @param enabled true to enable, false to disable
|
||||||
|
* @return true if operation was successful, false otherwise
|
||||||
|
*/
|
||||||
|
external fun setUidScannerEnabled(enabled: Boolean): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear UID scanner environment (force exit)
|
||||||
|
* This will forcefully stop all UID scanner operations and clear the environment
|
||||||
|
* @return true if operation was successful, false otherwise
|
||||||
|
*/
|
||||||
|
external fun clearUidScannerEnvironment(): Boolean
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private const val NON_ROOT_DEFAULT_PROFILE_KEY = "$"
|
private const val NON_ROOT_DEFAULT_PROFILE_KEY = "$"
|
||||||
private const val NOBODY_UID = 9999
|
private const val NOBODY_UID = 9999
|
||||||
|
|
||||||
@@ -107,9 +195,41 @@ object Natives {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun requireNewKernel(): Boolean {
|
fun requireNewKernel(): Boolean {
|
||||||
return version != -1 && version < MINIMAL_SUPPORTED_KERNEL
|
if (version != -1 && version < MINIMAL_SUPPORTED_KERNEL) return true
|
||||||
|
return isVersionLessThan(getFullVersion(), MINIMAL_SUPPORTED_KERNEL_FULL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
@Parcelize
|
||||||
|
@Keep
|
||||||
|
data class DynamicManagerConfig(
|
||||||
|
val size: Int = 0,
|
||||||
|
val hash: String = ""
|
||||||
|
) : Parcelable {
|
||||||
|
|
||||||
|
fun isValid(): Boolean {
|
||||||
|
return size > 0 && hash.length == 64 && hash.all {
|
||||||
|
it in '0'..'9' || it in 'a'..'f' || it in 'A'..'F'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
@Parcelize
|
||||||
|
@Keep
|
||||||
|
data class ManagersList(
|
||||||
|
val count: Int = 0,
|
||||||
|
val managers: List<ManagerInfo> = emptyList()
|
||||||
|
) : Parcelable
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
@Parcelize
|
||||||
|
@Keep
|
||||||
|
data class ManagerInfo(
|
||||||
|
val uid: Int = 0,
|
||||||
|
val signatureIndex: Int = 0
|
||||||
|
) : Parcelable
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@Keep
|
@Keep
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.sukisu.ultra.ui.screen
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.os.Process.myUid
|
||||||
import android.system.Os
|
import android.system.Os
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
@@ -71,10 +72,7 @@ import com.sukisu.ultra.getKernelVersion
|
|||||||
import com.sukisu.ultra.ui.component.DropdownItem
|
import com.sukisu.ultra.ui.component.DropdownItem
|
||||||
import com.sukisu.ultra.ui.component.RebootListPopup
|
import com.sukisu.ultra.ui.component.RebootListPopup
|
||||||
import com.sukisu.ultra.ui.component.rememberConfirmDialog
|
import com.sukisu.ultra.ui.component.rememberConfirmDialog
|
||||||
import com.sukisu.ultra.ui.util.checkNewVersion
|
import com.sukisu.ultra.ui.util.*
|
||||||
import com.sukisu.ultra.ui.util.getModuleCount
|
|
||||||
import com.sukisu.ultra.ui.util.getSELinuxStatus
|
|
||||||
import com.sukisu.ultra.ui.util.getSuperuserCount
|
|
||||||
import com.sukisu.ultra.ui.util.module.LatestVersionInfo
|
import com.sukisu.ultra.ui.util.module.LatestVersionInfo
|
||||||
import com.sukisu.ultra.ui.util.reboot
|
import com.sukisu.ultra.ui.util.reboot
|
||||||
import com.sukisu.ultra.ui.util.rootAvailable
|
import com.sukisu.ultra.ui.util.rootAvailable
|
||||||
@@ -323,7 +321,7 @@ private fun StatusCard(
|
|||||||
val workingMode = when (lkmMode) {
|
val workingMode = when (lkmMode) {
|
||||||
null -> ""
|
null -> ""
|
||||||
true -> " <LKM>"
|
true -> " <LKM>"
|
||||||
else -> " <GKI>"
|
else -> " <Built-in>"
|
||||||
}
|
}
|
||||||
|
|
||||||
val workingText = "${stringResource(id = R.string.home_working)}$workingMode$safeMode"
|
val workingText = "${stringResource(id = R.string.home_working)}$workingMode$safeMode"
|
||||||
@@ -590,6 +588,8 @@ fun DonateCard() {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun InfoCard() {
|
private fun InfoCard() {
|
||||||
|
val manualHookText = stringResource(R.string.manual_hook)
|
||||||
|
val inlineHookText = stringResource(R.string.inline_hook)
|
||||||
@Composable
|
@Composable
|
||||||
fun InfoText(
|
fun InfoText(
|
||||||
title: String,
|
title: String,
|
||||||
@@ -609,10 +609,29 @@ private fun InfoCard() {
|
|||||||
modifier = Modifier.padding(top = 2.dp, bottom = bottomPadding)
|
modifier = Modifier.padding(top = 2.dp, bottom = bottomPadding)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
|
val uname = Os.uname()
|
||||||
|
val managerVersion = getManagerVersion(context)
|
||||||
|
val susfsPair by produceState(initialValue = "" to "") {
|
||||||
|
value = withContext(Dispatchers.IO) {
|
||||||
|
val rawFeature = getSuSFSFeatures()
|
||||||
|
val status = if (rawFeature.isNotEmpty() && !rawFeature.startsWith("[-]")) "Supported" else rawFeature
|
||||||
|
if (status == "Supported") {
|
||||||
|
val version = getSuSFSVersion()
|
||||||
|
val hook = when (Natives.getHookType()) {
|
||||||
|
"Manual" -> "($manualHookText)"
|
||||||
|
"Inline" -> "($inlineHookText)"
|
||||||
|
else -> "(${Natives.getHookType()})"
|
||||||
|
}
|
||||||
|
status to "$version $hook".trim()
|
||||||
|
} else {
|
||||||
|
"" to ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Card {
|
Card {
|
||||||
val context = LocalContext.current
|
|
||||||
val uname = Os.uname()
|
|
||||||
val managerVersion = getManagerVersion(context)
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -626,15 +645,46 @@ private fun InfoCard() {
|
|||||||
title = stringResource(R.string.home_manager_version),
|
title = stringResource(R.string.home_manager_version),
|
||||||
content = "${managerVersion.first} (${managerVersion.second})"
|
content = "${managerVersion.first} (${managerVersion.second})"
|
||||||
)
|
)
|
||||||
|
val managersList = remember { Natives.getManagersList() }
|
||||||
|
val dynamicValid = remember { Natives.getDynamicManager()?.isValid() == true }
|
||||||
|
if (dynamicValid && managersList != null) {
|
||||||
|
val signatureMap = managersList.managers.groupBy { it.signatureIndex }
|
||||||
|
val showDetailed = signatureMap.size > 1 || signatureMap.keys.firstOrNull() != 0
|
||||||
|
if (showDetailed) {
|
||||||
|
val managersText = buildString {
|
||||||
|
signatureMap.toSortedMap().forEach { (idx, list) ->
|
||||||
|
append(list.joinToString(", ") { "UID: ${it.uid}" })
|
||||||
|
append(
|
||||||
|
when (idx) {
|
||||||
|
0 -> " (${stringResource(R.string.default_signature)})"
|
||||||
|
100 -> " (${stringResource(R.string.dynamic_managerature)})"
|
||||||
|
else -> " (${stringResource(R.string.signature_index, idx)})"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
append(" | ")
|
||||||
|
}
|
||||||
|
}.trimEnd(' ', '|')
|
||||||
|
InfoText(
|
||||||
|
title = stringResource(R.string.multi_manager_list),
|
||||||
|
content = managersText
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
InfoText(
|
InfoText(
|
||||||
title = stringResource(R.string.home_fingerprint),
|
title = stringResource(R.string.home_fingerprint),
|
||||||
content = Build.FINGERPRINT
|
content = Build.FINGERPRINT
|
||||||
)
|
)
|
||||||
InfoText(
|
InfoText(
|
||||||
title = stringResource(R.string.home_selinux_status),
|
title = stringResource(R.string.home_selinux_status),
|
||||||
content = getSELinuxStatus(),
|
content = getSELinuxStatus()
|
||||||
bottomPadding = 0.dp
|
|
||||||
)
|
)
|
||||||
|
if (susfsPair.first == "Supported" && susfsPair.second.isNotEmpty()) {
|
||||||
|
InfoText(
|
||||||
|
title = stringResource(R.string.home_susfs_version),
|
||||||
|
content = susfsPair.second,
|
||||||
|
bottomPadding = 0.dp
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,901 @@
|
|||||||
|
package com.sukisu.ultra.ui.screen
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Process.myUid
|
||||||
|
import androidx.compose.animation.*
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
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.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.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 androidx.core.content.edit
|
||||||
|
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.util.*
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import top.yukonga.miuix.kmp.basic.*
|
||||||
|
import top.yukonga.miuix.kmp.extra.SuperArrow
|
||||||
|
import top.yukonga.miuix.kmp.extra.SuperDialog
|
||||||
|
import top.yukonga.miuix.kmp.icon.MiuixIcons
|
||||||
|
import top.yukonga.miuix.kmp.icon.icons.useful.Back
|
||||||
|
import top.yukonga.miuix.kmp.icon.icons.useful.Delete
|
||||||
|
import top.yukonga.miuix.kmp.icon.icons.useful.Refresh
|
||||||
|
import top.yukonga.miuix.kmp.icon.icons.basic.Search
|
||||||
|
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||||
|
import java.time.*
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
|
private val SPACING_SMALL = 4.dp
|
||||||
|
private val SPACING_MEDIUM = 8.dp
|
||||||
|
private val SPACING_LARGE = 16.dp
|
||||||
|
|
||||||
|
private const val PAGE_SIZE = 10000
|
||||||
|
private const val MAX_TOTAL_LOGS = 100000
|
||||||
|
|
||||||
|
private const val LOGS_PATCH = "/data/adb/ksu/log/sulog.log"
|
||||||
|
|
||||||
|
data class LogEntry(
|
||||||
|
val timestamp: String,
|
||||||
|
val type: LogType,
|
||||||
|
val uid: String,
|
||||||
|
val comm: String,
|
||||||
|
val details: String,
|
||||||
|
val pid: 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) {
|
||||||
|
SU_GRANT("SU_GRANT", Color(0xFF4CAF50)),
|
||||||
|
SU_EXEC("SU_EXEC", Color(0xFF2196F3)),
|
||||||
|
PERM_CHECK("PERM_CHECK", Color(0xFFFF9800)),
|
||||||
|
SYSCALL("SYSCALL", Color(0xFF00BCD4)),
|
||||||
|
MANAGER_OP("MANAGER_OP", Color(0xFF9C27B0)),
|
||||||
|
UNKNOWN("UNKNOWN", Color(0xFF757575))
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class LogExclType(val displayName: String, val color: Color) {
|
||||||
|
CURRENT_APP("Current app", Color(0xFF9E9E9E)),
|
||||||
|
PRCTL_STAR("prctl_*", Color(0xFF00BCD4)),
|
||||||
|
PRCTL_UNKNOWN("prctl_unknown", Color(0xFF00BCD4)),
|
||||||
|
SETUID("setuid", Color(0xFF00BCD4))
|
||||||
|
}
|
||||||
|
|
||||||
|
private val utcFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
|
||||||
|
private val localFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
|
||||||
|
|
||||||
|
private fun saveExcludedSubTypes(context: Context, types: Set<LogExclType>) {
|
||||||
|
val prefs = context.getSharedPreferences("sulog", Context.MODE_PRIVATE)
|
||||||
|
val nameSet = types.map { it.name }.toSet()
|
||||||
|
prefs.edit { putStringSet("excluded_subtypes", nameSet) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadExcludedSubTypes(context: Context): Set<LogExclType> {
|
||||||
|
val prefs = context.getSharedPreferences("sulog", Context.MODE_PRIVATE)
|
||||||
|
val nameSet = prefs.getStringSet("excluded_subtypes", emptySet()) ?: emptySet()
|
||||||
|
return nameSet.mapNotNull { name ->
|
||||||
|
LogExclType.entries.firstOrNull { it.name == name }
|
||||||
|
}.toSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Destination<RootGraph>
|
||||||
|
@Composable
|
||||||
|
fun LogViewer(navigator: DestinationsNavigator) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
var logEntries by remember { mutableStateOf<List<LogEntry>>(emptyList()) }
|
||||||
|
var isLoading by remember { mutableStateOf(false) }
|
||||||
|
var filterType by rememberSaveable { mutableStateOf<LogType?>(null) }
|
||||||
|
var searchQuery by rememberSaveable { mutableStateOf("") }
|
||||||
|
var showSearchBar by rememberSaveable { mutableStateOf(false) }
|
||||||
|
var pageInfo by remember { mutableStateOf(LogPageInfo()) }
|
||||||
|
var lastLogFileHash by remember { mutableStateOf("") }
|
||||||
|
val currentUid = remember { myUid().toString() }
|
||||||
|
|
||||||
|
val initialExcluded = remember {
|
||||||
|
loadExcludedSubTypes(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
var excludedSubTypes by rememberSaveable { mutableStateOf(initialExcluded) }
|
||||||
|
|
||||||
|
LaunchedEffect(excludedSubTypes) {
|
||||||
|
saveExcludedSubTypes(context, excludedSubTypes)
|
||||||
|
}
|
||||||
|
|
||||||
|
val filteredEntries = remember(
|
||||||
|
logEntries, filterType, searchQuery, excludedSubTypes
|
||||||
|
) {
|
||||||
|
logEntries.filter { entry ->
|
||||||
|
val matchesSearch = searchQuery.isEmpty() ||
|
||||||
|
entry.comm.contains(searchQuery, ignoreCase = true) ||
|
||||||
|
entry.details.contains(searchQuery, ignoreCase = true) ||
|
||||||
|
entry.uid.contains(searchQuery, ignoreCase = true)
|
||||||
|
|
||||||
|
if (LogExclType.CURRENT_APP in excludedSubTypes && entry.uid == currentUid) return@filter false
|
||||||
|
|
||||||
|
if (entry.type == LogType.SYSCALL) {
|
||||||
|
val detail = entry.details
|
||||||
|
if (LogExclType.PRCTL_STAR in excludedSubTypes && detail.startsWith("Syscall: prctl") && !detail.startsWith("Syscall: prctl_unknown")) return@filter false
|
||||||
|
if (LogExclType.PRCTL_UNKNOWN in excludedSubTypes && detail.startsWith("Syscall: prctl_unknown")) return@filter false
|
||||||
|
if (LogExclType.SETUID in excludedSubTypes && detail.startsWith("Syscall: setuid")) return@filter false
|
||||||
|
}
|
||||||
|
|
||||||
|
val matchesFilter = filterType == null || entry.type == filterType
|
||||||
|
matchesFilter && matchesSearch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var showClearDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val loadPage: (Int, Boolean) -> Unit = { page, forceRefresh ->
|
||||||
|
scope.launch {
|
||||||
|
if (isLoading) return@launch
|
||||||
|
|
||||||
|
isLoading = true
|
||||||
|
try {
|
||||||
|
loadLogsWithPagination(
|
||||||
|
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(Unit) {
|
||||||
|
while (true) {
|
||||||
|
delay(5_000)
|
||||||
|
if (!isLoading) {
|
||||||
|
scope.launch {
|
||||||
|
val hasNewLogs = checkForNewLogs(lastLogFileHash)
|
||||||
|
if (hasNewLogs) {
|
||||||
|
loadPage(0, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
loadPage(0, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = stringResource(R.string.log_viewer_title),
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(
|
||||||
|
onClick = { navigator.navigateUp() },
|
||||||
|
modifier = Modifier.padding(start = 12.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = MiuixIcons.Useful.Back,
|
||||||
|
contentDescription = stringResource(R.string.log_viewer_back)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
IconButton(onClick = { showSearchBar = !showSearchBar }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = MiuixIcons.Basic.Search,
|
||||||
|
contentDescription = stringResource(R.string.log_viewer_search)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IconButton(onClick = onManualRefresh) {
|
||||||
|
Icon(
|
||||||
|
imageVector = MiuixIcons.Useful.Refresh,
|
||||||
|
contentDescription = stringResource(R.string.log_viewer_refresh)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IconButton(
|
||||||
|
onClick = { showClearDialog = true },
|
||||||
|
modifier = Modifier.padding(end = 12.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = MiuixIcons.Useful.Delete,
|
||||||
|
contentDescription = stringResource(R.string.log_viewer_clear_logs)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues)
|
||||||
|
) {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = showSearchBar,
|
||||||
|
enter = fadeIn() + expandVertically(),
|
||||||
|
exit = fadeOut() + shrinkVertically()
|
||||||
|
) {
|
||||||
|
TextField(
|
||||||
|
value = searchQuery,
|
||||||
|
onValueChange = { searchQuery = it },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = SPACING_LARGE, vertical = SPACING_MEDIUM),
|
||||||
|
label = stringResource(R.string.log_viewer_search_placeholder)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
LogControlPanel(
|
||||||
|
filterType = filterType,
|
||||||
|
onFilterTypeSelected = { filterType = it },
|
||||||
|
logCount = filteredEntries.size,
|
||||||
|
totalCount = logEntries.size,
|
||||||
|
pageInfo = pageInfo,
|
||||||
|
excludedSubTypes = excludedSubTypes,
|
||||||
|
onExcludeToggle = { excl ->
|
||||||
|
excludedSubTypes = if (excl in excludedSubTypes)
|
||||||
|
excludedSubTypes - excl
|
||||||
|
else
|
||||||
|
excludedSubTypes + excl
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isLoading && logEntries.isEmpty()) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
}
|
||||||
|
} else if (filteredEntries.isEmpty()) {
|
||||||
|
EmptyLogState(
|
||||||
|
hasLogs = logEntries.isNotEmpty(),
|
||||||
|
onRefresh = onManualRefresh
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
LogList(
|
||||||
|
entries = filteredEntries,
|
||||||
|
pageInfo = pageInfo,
|
||||||
|
isLoading = isLoading,
|
||||||
|
onLoadMore = loadNextPage,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val showClearDialogState = remember { mutableStateOf(showClearDialog) }
|
||||||
|
|
||||||
|
LaunchedEffect(showClearDialog) {
|
||||||
|
showClearDialogState.value = showClearDialog
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(showClearDialogState.value) {
|
||||||
|
showClearDialog = showClearDialogState.value
|
||||||
|
}
|
||||||
|
|
||||||
|
SuperDialog(
|
||||||
|
show = showClearDialogState,
|
||||||
|
onDismissRequest = { showClearDialog = false },
|
||||||
|
content = {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 24.dp, bottom = 12.dp),
|
||||||
|
text = stringResource(R.string.log_viewer_clear_logs),
|
||||||
|
style = MiuixTheme.textStyles.title4,
|
||||||
|
textAlign = androidx.compose.ui.text.style.TextAlign.Center,
|
||||||
|
color = MiuixTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 24.dp, vertical = 12.dp),
|
||||||
|
text = stringResource(R.string.log_viewer_clear_logs_confirm),
|
||||||
|
style = MiuixTheme.textStyles.body2,
|
||||||
|
textAlign = androidx.compose.ui.text.style.TextAlign.Center,
|
||||||
|
color = MiuixTheme.colorScheme.onSurfaceVariantSummary
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 24.dp, vertical = 12.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
TextButton(
|
||||||
|
text = stringResource(android.R.string.cancel),
|
||||||
|
onClick = { showClearDialog = false },
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
TextButton(
|
||||||
|
text = stringResource(android.R.string.ok),
|
||||||
|
onClick = {
|
||||||
|
showClearDialog = false
|
||||||
|
scope.launch {
|
||||||
|
clearLogs()
|
||||||
|
loadPage(0, true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
colors = ButtonDefaults.textButtonColorsPrimary()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val CONTROL_PANEL_SPACING_SMALL = 4.dp
|
||||||
|
private val CONTROL_PANEL_SPACING_MEDIUM = 8.dp
|
||||||
|
private val CONTROL_PANEL_SPACING_LARGE = 12.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun LogControlPanel(
|
||||||
|
filterType: LogType?,
|
||||||
|
onFilterTypeSelected: (LogType?) -> Unit,
|
||||||
|
logCount: Int,
|
||||||
|
totalCount: Int,
|
||||||
|
pageInfo: LogPageInfo,
|
||||||
|
excludedSubTypes: Set<LogExclType>,
|
||||||
|
onExcludeToggle: (LogExclType) -> Unit
|
||||||
|
) {
|
||||||
|
var isExpanded by rememberSaveable { mutableStateOf(true) }
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = SPACING_LARGE, vertical = SPACING_MEDIUM)
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
SuperArrow(
|
||||||
|
title = stringResource(R.string.log_viewer_settings),
|
||||||
|
onClick = { isExpanded = !isExpanded },
|
||||||
|
rightText = if (isExpanded)
|
||||||
|
stringResource(R.string.log_viewer_collapse)
|
||||||
|
else
|
||||||
|
stringResource(R.string.log_viewer_expand)
|
||||||
|
)
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = isExpanded,
|
||||||
|
enter = expandVertically() + fadeIn(),
|
||||||
|
exit = shrinkVertically() + fadeOut()
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(horizontal = SPACING_MEDIUM)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.log_viewer_filter_type),
|
||||||
|
style = MiuixTheme.textStyles.subtitle,
|
||||||
|
color = MiuixTheme.colorScheme.onSurfaceVariantActions
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(CONTROL_PANEL_SPACING_MEDIUM))
|
||||||
|
LazyRow(horizontalArrangement = Arrangement.spacedBy(CONTROL_PANEL_SPACING_MEDIUM)) {
|
||||||
|
item {
|
||||||
|
FilterChip(
|
||||||
|
text = stringResource(R.string.log_viewer_all_types),
|
||||||
|
selected = filterType == null,
|
||||||
|
onClick = { onFilterTypeSelected(null) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
items(LogType.entries.toTypedArray()) { type ->
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(6.dp)
|
||||||
|
.background(type.color, RoundedCornerShape(3.dp))
|
||||||
|
)
|
||||||
|
FilterChip(
|
||||||
|
text = type.displayName,
|
||||||
|
selected = filterType == type,
|
||||||
|
onClick = { onFilterTypeSelected(if (filterType == type) null else type) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(CONTROL_PANEL_SPACING_LARGE))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.log_viewer_exclude_subtypes),
|
||||||
|
style = MiuixTheme.textStyles.subtitle,
|
||||||
|
color = MiuixTheme.colorScheme.onSurfaceVariantActions
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(CONTROL_PANEL_SPACING_MEDIUM))
|
||||||
|
LazyRow(horizontalArrangement = Arrangement.spacedBy(CONTROL_PANEL_SPACING_MEDIUM)) {
|
||||||
|
items(LogExclType.entries.toTypedArray()) { excl ->
|
||||||
|
val label = if (excl == LogExclType.CURRENT_APP)
|
||||||
|
stringResource(R.string.log_viewer_exclude_current_app)
|
||||||
|
else excl.displayName
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(6.dp)
|
||||||
|
.background(excl.color, RoundedCornerShape(3.dp))
|
||||||
|
)
|
||||||
|
FilterChip(
|
||||||
|
text = label,
|
||||||
|
selected = excl in excludedSubTypes,
|
||||||
|
onClick = { onExcludeToggle(excl) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(CONTROL_PANEL_SPACING_LARGE))
|
||||||
|
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(CONTROL_PANEL_SPACING_SMALL)) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.log_viewer_showing_entries, logCount, totalCount),
|
||||||
|
style = MiuixTheme.textStyles.body2,
|
||||||
|
color = MiuixTheme.colorScheme.onSurfaceVariantSummary
|
||||||
|
)
|
||||||
|
if (pageInfo.totalPages > 0) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
R.string.log_viewer_page_info,
|
||||||
|
pageInfo.currentPage + 1,
|
||||||
|
pageInfo.totalPages,
|
||||||
|
pageInfo.totalLogs
|
||||||
|
),
|
||||||
|
style = MiuixTheme.textStyles.body2,
|
||||||
|
color = MiuixTheme.colorScheme.onSurfaceVariantSummary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (pageInfo.totalLogs >= MAX_TOTAL_LOGS) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.log_viewer_too_many_logs, MAX_TOTAL_LOGS),
|
||||||
|
style = MiuixTheme.textStyles.body2,
|
||||||
|
color = Color(0xFFE53935)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(CONTROL_PANEL_SPACING_LARGE))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun LogList(
|
||||||
|
entries: List<LogEntry>,
|
||||||
|
pageInfo: LogPageInfo,
|
||||||
|
isLoading: Boolean,
|
||||||
|
onLoadMore: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
state = listState,
|
||||||
|
modifier = modifier,
|
||||||
|
contentPadding = PaddingValues(horizontal = SPACING_LARGE, vertical = SPACING_MEDIUM),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(SPACING_MEDIUM)
|
||||||
|
) {
|
||||||
|
items(entries) { 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()
|
||||||
|
) {
|
||||||
|
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 = MiuixTheme.textStyles.body2,
|
||||||
|
color = MiuixTheme.colorScheme.onSurfaceVariantSummary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun LogEntryCard(entry: LogEntry) {
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
onClick = { expanded = !expanded }
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(SPACING_LARGE)
|
||||||
|
) {
|
||||||
|
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 = MiuixTheme.textStyles.subtitle,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = entry.timestamp,
|
||||||
|
style = MiuixTheme.textStyles.body2,
|
||||||
|
color = MiuixTheme.colorScheme.onSurfaceVariantSummary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(SPACING_SMALL))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "UID: ${entry.uid}",
|
||||||
|
style = MiuixTheme.textStyles.body2,
|
||||||
|
color = MiuixTheme.colorScheme.onSurfaceVariantSummary
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "PID: ${entry.pid}",
|
||||||
|
style = MiuixTheme.textStyles.body2,
|
||||||
|
color = MiuixTheme.colorScheme.onSurfaceVariantSummary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = entry.comm,
|
||||||
|
style = MiuixTheme.textStyles.body1,
|
||||||
|
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 = MiuixTheme.textStyles.body2,
|
||||||
|
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
|
||||||
|
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()
|
||||||
|
Spacer(modifier = Modifier.height(SPACING_MEDIUM))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.log_viewer_raw_log),
|
||||||
|
style = MiuixTheme.textStyles.subtitle,
|
||||||
|
color = MiuixTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(SPACING_SMALL))
|
||||||
|
Text(
|
||||||
|
text = entry.rawLine,
|
||||||
|
style = MiuixTheme.textStyles.body2,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
color = MiuixTheme.colorScheme.onSurfaceVariantSummary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun EmptyLogState(
|
||||||
|
hasLogs: Boolean,
|
||||||
|
onRefresh: () -> Unit
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(SPACING_LARGE)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
if (hasLogs) R.string.log_viewer_no_matching_logs
|
||||||
|
else R.string.log_viewer_no_logs
|
||||||
|
),
|
||||||
|
style = MiuixTheme.textStyles.headline2,
|
||||||
|
color = MiuixTheme.colorScheme.onSurfaceVariantSummary
|
||||||
|
)
|
||||||
|
Button(
|
||||||
|
onClick = onRefresh
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.log_viewer_refresh))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun checkForNewLogs(lastHash: String): Boolean {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val shell = getRootShell()
|
||||||
|
val logPath = "/data/adb/ksu/log/sulog.log"
|
||||||
|
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(
|
||||||
|
page: Int,
|
||||||
|
forceRefresh: Boolean,
|
||||||
|
lastHash: String,
|
||||||
|
onLoaded: (List<LogEntry>, LogPageInfo, String) -> Unit
|
||||||
|
) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val shell = getRootShell()
|
||||||
|
val statResult = runCmd(shell, "stat -c '%Y %s' $LOGS_PATCH 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 < $LOGS_PATCH 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' $LOGS_PATCH 2>/dev/null || echo ''")
|
||||||
|
val entries = parseLogEntries(result)
|
||||||
|
|
||||||
|
val hasMore = endLine < totalLines
|
||||||
|
val pageInfo = LogPageInfo(page, totalPages, effectiveTotal, hasMore)
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
onLoaded(entries, pageInfo, currentHash)
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
onLoaded(emptyList(), LogPageInfo(), lastHash)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun clearLogs() {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val shell = getRootShell()
|
||||||
|
runCmd(shell, "echo '' > $LOGS_PATCH")
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseLogEntries(logContent: String): List<LogEntry> {
|
||||||
|
if (logContent.isBlank()) return emptyList()
|
||||||
|
|
||||||
|
val entries = logContent.lines()
|
||||||
|
.filter { it.isNotBlank() && it.startsWith("[") }
|
||||||
|
.mapNotNull { line ->
|
||||||
|
try {
|
||||||
|
parseLogLine(line)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries.reversed()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun utcToLocal(utc: String): String {
|
||||||
|
return try {
|
||||||
|
val instant = LocalDateTime.parse(utc, utcFormatter).atOffset(ZoneOffset.UTC).toInstant()
|
||||||
|
val local = instant.atZone(ZoneId.systemDefault())
|
||||||
|
local.format(localFormatter)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
utc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseLogLine(line: String): LogEntry? {
|
||||||
|
val timestampRegex = """\[(.*?)]""".toRegex()
|
||||||
|
val timestampMatch = timestampRegex.find(line) ?: return null
|
||||||
|
val timestamp = utcToLocal(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
|
||||||
|
"SYSCALL" -> LogType.SYSCALL
|
||||||
|
"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.SYSCALL -> {
|
||||||
|
val syscall = extractValue(details, "SYSCALL") ?: ""
|
||||||
|
val args = extractValue(details, "ARGS") ?: ""
|
||||||
|
"Syscall: $syscall, Args: $args"
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun FilterChip(
|
||||||
|
text: String,
|
||||||
|
selected: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
TextButton(
|
||||||
|
text = text,
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = modifier,
|
||||||
|
colors = if (selected) {
|
||||||
|
ButtonDefaults.textButtonColorsPrimary()
|
||||||
|
} else {
|
||||||
|
ButtonDefaults.textButtonColors()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -47,6 +47,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.AboutScreenDestination
|
import com.ramcosta.composedestinations.generated.destinations.AboutScreenDestination
|
||||||
import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination
|
import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination
|
||||||
|
import com.ramcosta.composedestinations.generated.destinations.LogViewerDestination
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
import dev.chrisbanes.haze.HazeState
|
import dev.chrisbanes.haze.HazeState
|
||||||
import dev.chrisbanes.haze.HazeStyle
|
import dev.chrisbanes.haze.HazeStyle
|
||||||
@@ -112,6 +113,7 @@ fun SettingPager(
|
|||||||
val uninstallDialog = UninstallDialog(showUninstallDialog, navigator)
|
val uninstallDialog = UninstallDialog(showUninstallDialog, navigator)
|
||||||
val showSendLogDialog = rememberSaveable { mutableStateOf(false) }
|
val showSendLogDialog = rememberSaveable { mutableStateOf(false) }
|
||||||
val sendLogDialog = SendLogDialog(showSendLogDialog, loadingDialog)
|
val sendLogDialog = SendLogDialog(showSendLogDialog, loadingDialog)
|
||||||
|
var isSuLogEnabled by remember { mutableStateOf(Natives.isSuLogEnabled()) }
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -369,6 +371,59 @@ fun SettingPager(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var SuLogMode by rememberSaveable {
|
||||||
|
mutableIntStateOf(
|
||||||
|
run {
|
||||||
|
val currentEnabled = Natives.isSuLogEnabled()
|
||||||
|
val savedPersist = prefs.getInt("sulog_mode", 0)
|
||||||
|
if (savedPersist == 2) 2 else if (!currentEnabled) 1 else 0
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SuperDropdown(
|
||||||
|
title = stringResource(id = R.string.settings_disable_sulog),
|
||||||
|
summary = stringResource(id = R.string.settings_disable_sulog_summary),
|
||||||
|
items = modeItems,
|
||||||
|
leftAction = {
|
||||||
|
Icon(
|
||||||
|
Icons.Rounded.RemoveCircle,
|
||||||
|
modifier = Modifier.padding(end = 16.dp),
|
||||||
|
contentDescription = stringResource(id = R.string.settings_disable_sulog),
|
||||||
|
tint = colorScheme.onBackground
|
||||||
|
)
|
||||||
|
},
|
||||||
|
selectedIndex = SuLogMode,
|
||||||
|
onSelectedIndexChange = { index ->
|
||||||
|
when (index) {
|
||||||
|
// Default: enable and save to persist
|
||||||
|
0 -> if (Natives.setSuLogEnabled(true)) {
|
||||||
|
execKsud("feature save", true)
|
||||||
|
prefs.edit { putInt("sulog_mode", 0) }
|
||||||
|
SuLogMode = 0
|
||||||
|
isSuLogEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Temporarily disable: save enabled state first, then disable
|
||||||
|
1 -> if (Natives.setSuLogEnabled(true)) {
|
||||||
|
execKsud("feature save", true)
|
||||||
|
if (Natives.setSuLogEnabled(false)) {
|
||||||
|
prefs.edit { putInt("sulog_mode", 0) }
|
||||||
|
SuLogMode = 1
|
||||||
|
isSuLogEnabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Permanently disable: disable and save
|
||||||
|
2 -> if (Natives.setSuLogEnabled(false)) {
|
||||||
|
execKsud("feature save", true)
|
||||||
|
prefs.edit { putInt("sulog_mode", 2) }
|
||||||
|
SuLogMode = 2
|
||||||
|
isSuLogEnabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
@@ -452,6 +507,24 @@ fun SettingPager(
|
|||||||
.padding(vertical = 12.dp)
|
.padding(vertical = 12.dp)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
|
if (isSuLogEnabled) {
|
||||||
|
val sulog = stringResource(id = R.string.log_viewer_view_logs)
|
||||||
|
SuperArrow(
|
||||||
|
title = sulog,
|
||||||
|
leftAction = {
|
||||||
|
Icon(
|
||||||
|
Icons.Rounded.BugReport,
|
||||||
|
modifier = Modifier.padding(end = 16.dp),
|
||||||
|
contentDescription = sulog,
|
||||||
|
tint = colorScheme.onBackground
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
navigator.navigate(LogViewerDestination) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
SuperArrow(
|
SuperArrow(
|
||||||
title = stringResource(id = R.string.send_log),
|
title = stringResource(id = R.string.send_log),
|
||||||
leftAction = {
|
leftAction = {
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ fun download(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun checkNewVersion(): LatestVersionInfo {
|
fun checkNewVersion(): LatestVersionInfo {
|
||||||
val url = "https://api.github.com/repos/tiann/KernelSU/releases/latest"
|
val url = "https://api.github.com/repos/SukiSU-Ultra/SukiSU-Ultra/releases/latest"
|
||||||
// default null value if failed
|
// default null value if failed
|
||||||
val defaultValue = LatestVersionInfo()
|
val defaultValue = LatestVersionInfo()
|
||||||
runCatching {
|
runCatching {
|
||||||
|
|||||||
@@ -472,3 +472,251 @@ fun restartApp(packageName: String) {
|
|||||||
forceStopApp(packageName)
|
forceStopApp(packageName)
|
||||||
launchApp(packageName)
|
launchApp(packageName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KPM控制
|
||||||
|
fun loadKpmModule(path: String, args: String? = null): String {
|
||||||
|
val shell = getRootShell()
|
||||||
|
val cmd = "${getKsuDaemonPath()} kpm load $path ${args ?: ""}"
|
||||||
|
return ShellUtils.fastCmd(shell, cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unloadKpmModule(name: String): String {
|
||||||
|
val shell = getRootShell()
|
||||||
|
val cmd = "${getKsuDaemonPath()} kpm unload $name"
|
||||||
|
return ShellUtils.fastCmd(shell, cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getKpmModuleCount(): Int {
|
||||||
|
val shell = getRootShell()
|
||||||
|
val cmd = "${getKsuDaemonPath()} kpm num"
|
||||||
|
val result = ShellUtils.fastCmd(shell, cmd)
|
||||||
|
return result.trim().toIntOrNull() ?: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fun runCmd(shell: Shell, cmd: String): String {
|
||||||
|
return shell.newJob()
|
||||||
|
.add(cmd)
|
||||||
|
.to(mutableListOf<String>(), null)
|
||||||
|
.exec().out
|
||||||
|
.joinToString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun listKpmModules(): String {
|
||||||
|
val shell = getRootShell()
|
||||||
|
val cmd = "${getKsuDaemonPath()} kpm list"
|
||||||
|
return try {
|
||||||
|
runCmd(shell, cmd).trim()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to list KPM modules", e)
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getKpmModuleInfo(name: String): String {
|
||||||
|
val shell = getRootShell()
|
||||||
|
val cmd = "${getKsuDaemonPath()} kpm info $name"
|
||||||
|
return try {
|
||||||
|
runCmd(shell, cmd).trim()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to get KPM module info: $name", e)
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun controlKpmModule(name: String, args: String? = null): Int {
|
||||||
|
val shell = getRootShell()
|
||||||
|
val cmd = """${getKsuDaemonPath()} kpm control $name "${args ?: ""}""""
|
||||||
|
val result = runCmd(shell, cmd)
|
||||||
|
return result.trim().toIntOrNull() ?: -1
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getKpmVersion(): String {
|
||||||
|
val shell = getRootShell()
|
||||||
|
val cmd = "${getKsuDaemonPath()} kpm version"
|
||||||
|
val result = ShellUtils.fastCmd(shell, cmd)
|
||||||
|
return result.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSuSFSDaemonPath(): String {
|
||||||
|
return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libksu_susfs.so"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSuSFSVersion(): String {
|
||||||
|
val shell = getRootShell()
|
||||||
|
val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} show version")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSuSFSVariant(): String {
|
||||||
|
val shell = getRootShell()
|
||||||
|
val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} show variant")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSuSFSFeatures(): String {
|
||||||
|
val shell = getRootShell()
|
||||||
|
val cmd = "${getSuSFSDaemonPath()} show enabled_features"
|
||||||
|
return runCmd(shell, cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getZygiskImplement(): String {
|
||||||
|
val shell = getRootShell()
|
||||||
|
|
||||||
|
val zygiskModuleIds = listOf(
|
||||||
|
"zygisksu",
|
||||||
|
"rezygisk",
|
||||||
|
"shirokozygisk"
|
||||||
|
)
|
||||||
|
|
||||||
|
for (moduleId in zygiskModuleIds) {
|
||||||
|
val modulePath = "/data/adb/modules/$moduleId"
|
||||||
|
when {
|
||||||
|
ShellUtils.fastCmdResult(shell, "test -f $modulePath/module.prop && test ! -f $modulePath/disable") -> {
|
||||||
|
val result = ShellUtils.fastCmd(shell, "grep '^name=' $modulePath/module.prop | cut -d'=' -f2")
|
||||||
|
Log.i(TAG, "Zygisk implement: $result")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i(TAG, "Zygisk implement: None")
|
||||||
|
return "None"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getUidScannerDaemonPath(): String {
|
||||||
|
return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libuid_scanner.so"
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val targetPath = "/data/adb/uid_scanner"
|
||||||
|
fun ensureUidScannerExecutable(): Boolean {
|
||||||
|
val shell = getRootShell()
|
||||||
|
val uidScannerPath = getUidScannerDaemonPath()
|
||||||
|
if (!ShellUtils.fastCmdResult(shell, "test -f $targetPath")) {
|
||||||
|
val copyResult = ShellUtils.fastCmdResult(shell, "cp $uidScannerPath $targetPath")
|
||||||
|
if (!copyResult) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = ShellUtils.fastCmdResult(shell, "chmod 755 $targetPath")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setUidAutoScan(enabled: Boolean): Boolean {
|
||||||
|
val shell = getRootShell()
|
||||||
|
if (!ensureUidScannerExecutable()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val enableValue = if (enabled) 1 else 0
|
||||||
|
val cmd = "$targetPath --auto-scan $enableValue && $targetPath reload"
|
||||||
|
val result = ShellUtils.fastCmdResult(shell, cmd)
|
||||||
|
|
||||||
|
val throneResult = Natives.setUidScannerEnabled(enabled)
|
||||||
|
|
||||||
|
return result && throneResult
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setUidMultiUserScan(enabled: Boolean): Boolean {
|
||||||
|
val shell = getRootShell()
|
||||||
|
if (!ensureUidScannerExecutable()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val enableValue = if (enabled) 1 else 0
|
||||||
|
val cmd = "$targetPath --multi-user $enableValue && $targetPath reload"
|
||||||
|
val result = ShellUtils.fastCmdResult(shell, cmd)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getUidMultiUserScan(): Boolean {
|
||||||
|
val shell = getRootShell()
|
||||||
|
|
||||||
|
val cmd = "grep 'multi_user_scan=' /data/misc/user_uid/uid_scanner.conf | cut -d'=' -f2"
|
||||||
|
val result = ShellUtils.fastCmd(shell, cmd).trim()
|
||||||
|
|
||||||
|
return try {
|
||||||
|
result.toInt() == 1
|
||||||
|
} catch (_: NumberFormatException) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cleanRuntimeEnvironment(): Boolean {
|
||||||
|
val shell = getRootShell()
|
||||||
|
return try {
|
||||||
|
try {
|
||||||
|
ShellUtils.fastCmd(shell, "/data/adb/uid_scanner stop")
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
ShellUtils.fastCmdResult(shell, "rm -rf /data/misc/user_uid")
|
||||||
|
ShellUtils.fastCmdResult(shell, "rm -rf /data/adb/uid_scanner")
|
||||||
|
ShellUtils.fastCmdResult(shell, "rm -rf /data/adb/ksu/bin/user_uid")
|
||||||
|
ShellUtils.fastCmdResult(shell, "rm -rf /data/adb/service.d/uid_scanner.sh")
|
||||||
|
Natives.clearUidScannerEnvironment()
|
||||||
|
true
|
||||||
|
} catch (_: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readUidScannerFile(): Boolean {
|
||||||
|
val shell = getRootShell()
|
||||||
|
return try {
|
||||||
|
ShellUtils.fastCmd(shell, "cat /data/adb/ksu/.uid_scanner").trim() == "1"
|
||||||
|
} catch (_: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addUmountPath(path: String, flags: Int): Boolean {
|
||||||
|
val shell = getRootShell()
|
||||||
|
val flagsArg = if (flags >= 0) "--flags $flags" else ""
|
||||||
|
val cmd = "${getKsuDaemonPath()} umount add $path $flagsArg"
|
||||||
|
val result = ShellUtils.fastCmdResult(shell, cmd)
|
||||||
|
Log.i(TAG, "add umount path $path result: $result")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeUmountPath(path: String): Boolean {
|
||||||
|
val shell = getRootShell()
|
||||||
|
val cmd = "${getKsuDaemonPath()} umount remove $path"
|
||||||
|
val result = ShellUtils.fastCmdResult(shell, cmd)
|
||||||
|
Log.i(TAG, "remove umount path $path result: $result")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun listUmountPaths(): String {
|
||||||
|
val shell = getRootShell()
|
||||||
|
val cmd = "${getKsuDaemonPath()} umount list"
|
||||||
|
return try {
|
||||||
|
runCmd(shell, cmd).trim()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to list umount paths", e)
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearCustomUmountPaths(): Boolean {
|
||||||
|
val shell = getRootShell()
|
||||||
|
val cmd = "${getKsuDaemonPath()} umount clear-custom"
|
||||||
|
val result = ShellUtils.fastCmdResult(shell, cmd)
|
||||||
|
Log.i(TAG, "clear custom umount paths result: $result")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveUmountConfig(): Boolean {
|
||||||
|
val shell = getRootShell()
|
||||||
|
val cmd = "${getKsuDaemonPath()} umount save"
|
||||||
|
val result = ShellUtils.fastCmdResult(shell, cmd)
|
||||||
|
Log.i(TAG, "save umount config result: $result")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun applyUmountConfigToKernel(): Boolean {
|
||||||
|
val shell = getRootShell()
|
||||||
|
val cmd = "${getKsuDaemonPath()} umount apply"
|
||||||
|
val result = ShellUtils.fastCmdResult(shell, cmd)
|
||||||
|
Log.i(TAG, "apply umount config to kernel result: $result")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|||||||
@@ -158,4 +158,39 @@
|
|||||||
<string name="module_undo_uninstall_success">成功撤销卸载 %s</string>
|
<string name="module_undo_uninstall_success">成功撤销卸载 %s</string>
|
||||||
<string name="module_undo_uninstall_failed">撤销卸载 %s 失败</string>
|
<string name="module_undo_uninstall_failed">撤销卸载 %s 失败</string>
|
||||||
<string name="group_contains_apps">包含 %1$d 个应用</string>
|
<string name="group_contains_apps">包含 %1$d 个应用</string>
|
||||||
|
<string name="home_susfs_version">SuSFS 版本</string>
|
||||||
|
<string name="manual_hook">Manual Hook</string>
|
||||||
|
<string name="inline_hook">Inline Hook</string>
|
||||||
|
<string name="multi_manager_list">活跃管理器</string>
|
||||||
|
<string name="default_signature">SukiSU</string>
|
||||||
|
<string name="dynamic_managerature">Dynamic</string>
|
||||||
|
<string name="signature_index">签名 %1$d</string>
|
||||||
|
<string name="settings_disable_sulog"> 禁用超级用户日志 </string>
|
||||||
|
<string name="settings_disable_sulog_summary">禁用 KernelSU 超级用户访问记录</string>
|
||||||
|
<!-- Log Viewer -->
|
||||||
|
<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_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">查看 KernelSU 超级用户访问日志</string>
|
||||||
|
<string name="log_viewer_exclude_subtypes">排除子类型</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>
|
||||||
|
<string name="log_viewer_settings">设置</string>
|
||||||
|
<string name="log_viewer_collapse">收起</string>
|
||||||
|
<string name="log_viewer_expand">展开</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -160,4 +160,39 @@
|
|||||||
<string name="module_undo_uninstall_success">Successfully canceled uninstall of %s</string>
|
<string name="module_undo_uninstall_success">Successfully canceled uninstall of %s</string>
|
||||||
<string name="module_undo_uninstall_failed">Failed to undo uninstall: %s</string>
|
<string name="module_undo_uninstall_failed">Failed to undo uninstall: %s</string>
|
||||||
<string name="group_contains_apps">Contains %d apps</string>
|
<string name="group_contains_apps">Contains %d apps</string>
|
||||||
|
<string name="home_susfs_version">SuSFS Version</string>
|
||||||
|
<string name="manual_hook">Manual Hook</string>
|
||||||
|
<string name="inline_hook">Inline Hook</string>
|
||||||
|
<string name="multi_manager_list">Active Manager</string>
|
||||||
|
<string name="default_signature">SukiSU</string>
|
||||||
|
<string name="dynamic_managerature">Dynamic</string>
|
||||||
|
<string name="signature_index">Signature %1$d</string>
|
||||||
|
<string name="settings_disable_sulog">Disable superuser logging</string>
|
||||||
|
<string name="settings_disable_sulog_summary">Disable KernelSU superuser access logging</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_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 KernelSU superuser access logs</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_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>
|
||||||
|
<string name="log_viewer_settings">Settings</string>
|
||||||
|
<string name="log_viewer_collapse">Collapse</string>
|
||||||
|
<string name="log_viewer_expand">Expand</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user