manager: support meta module (#616)

This commit is contained in:
AlexLiuDev233
2025-11-22 00:24:01 +08:00
committed by GitHub
parent de6dc65a56
commit a772a0f82d
11 changed files with 241 additions and 10 deletions

View File

@@ -33,6 +33,8 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.activity.ComponentActivity
import androidx.compose.material.icons.outlined.Info
import androidx.compose.ui.platform.LocalUriHandler
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
@@ -52,8 +54,10 @@ import java.io.File
import java.text.SimpleDateFormat
import java.util.*
import androidx.core.content.edit
import com.sukisu.ultra.ui.component.rememberCustomDialog
import com.sukisu.ultra.ui.util.module.ModuleOperationUtils
import com.sukisu.ultra.ui.util.module.ModuleUtils
import com.topjohnwu.superuser.io.SuFile
/**
* @author ShirkNeko
@@ -152,6 +156,7 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
var tempText: String
val logContent = rememberSaveable { StringBuilder() }
var showFloatAction by rememberSaveable { mutableStateOf(false) }
var shouldWarningUserMetaModule by rememberSaveable { mutableStateOf(false) }
// 添加状态跟踪是否已经完成刷写
var hasFlashCompleted by rememberSaveable { mutableStateOf(false) }
var hasExecuted by rememberSaveable { mutableStateOf(false) }
@@ -170,6 +175,39 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
val logSavedString = stringResource(R.string.log_saved)
val installingModuleString = stringResource(R.string.installing_module)
val alertDialog = rememberCustomDialog { dismiss: () -> Unit ->
val uriHandler = LocalUriHandler.current
AlertDialog(
onDismissRequest = { dismiss() },
icon = {
Icon(Icons.Outlined.Info, contentDescription = null)
},
title = {
Row(modifier = Modifier
.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
Text(text = stringResource(R.string.warning_of_meta_module_title))
}
},
text = {
Text(text = stringResource(R.string.warning_of_meta_module_summary))
},
confirmButton = {
FilledTonalButton(onClick = { dismiss() }) {
Text(text = stringResource(id = android.R.string.ok))
}
},
dismissButton = {
OutlinedButton(onClick = {
uriHandler.openUri("https://kernelsu.org/guide/metamodule.html")
}) {
Text(text = stringResource(id = R.string.learn_more))
}
},
)
}
// 当前模块安装状态
val currentStatus = moduleInstallStatus.value
@@ -182,16 +220,19 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
totalModules = flashIt.uris.size,
currentModule = 1
)
shouldWarningUserMetaModule = false
hasFlashCompleted = false
hasExecuted = false
moduleVerificationMap.clear()
}
}
is FlashIt.FlashModuleUpdate -> {
shouldWarningUserMetaModule = false
hasUpdateCompleted = false
hasUpdateExecuted = false
}
else -> {
shouldWarningUserMetaModule = false
hasFlashCompleted = false
hasExecuted = false
}
@@ -240,10 +281,29 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
}
hasUpdateCompleted = true
if (!hasMetaModule() && code == 0) {
// 如果没安装 MetaModule且此模块需要挂载并且当前模块安装成功警告用户
scope.launch {
val mountOldDirectory = SuFile.open("/data/adb/modules/${getModuleIdFromUri(context,flashIt.uri)}/system")
val mountNewDirectory = SuFile.open("/data/adb/modules_update/${getModuleIdFromUri(context,flashIt.uri)}/system")
if (!(mountNewDirectory.isDirectory) && !(mountOldDirectory.isDirectory)) return@launch
shouldWarningUserMetaModule = true
alertDialog.show()
shouldWarningUserMetaModule = false
}
}
// 如果是外部安装或需要自动退出的模块更新且不需要重启,延迟后自动返回
if (isExternalInstall || shouldAutoExit) {
scope.launch {
while (shouldWarningUserMetaModule) {
kotlinx.coroutines.delay(100)
}
kotlinx.coroutines.delay(1000)
while (shouldWarningUserMetaModule) {
kotlinx.coroutines.delay(100)
}
if (shouldAutoExit) {
val sharedPref = context.getSharedPreferences("kernel_flash_prefs", Context.MODE_PRIVATE)
sharedPref.edit { remove("auto_exit_after_flash") }
@@ -334,6 +394,38 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
}
hasFlashCompleted = true
if (!hasMetaModule() && code == 0) {
// 没有 MetaModule且安装成功检查此模块是否有自动挂载
scope.launch {
var mountOldDirectory : File
var mountNewDirectory : File
when (flashIt) {
is FlashIt.FlashModules -> {
mountOldDirectory = SuFile.open("/data/adb/modules/${getModuleIdFromUri(context,flashIt.uris[flashIt.currentIndex])}/system")
mountNewDirectory = SuFile.open("/data/adb/modules_update/${getModuleIdFromUri(context,flashIt.uris[flashIt.currentIndex])}/system")
}
is FlashIt.FlashModule -> {
mountOldDirectory = SuFile.open("/data/adb/modules/${getModuleIdFromUri(context,flashIt.uri)}/system")
mountNewDirectory = SuFile.open("/data/adb/modules_update/${getModuleIdFromUri(context,flashIt.uri)}/system")
}
is FlashIt.FlashModuleUpdate -> {
mountOldDirectory = SuFile.open("/data/adb/modules/${getModuleIdFromUri(context,flashIt.uri)}/system")
mountNewDirectory = SuFile.open("/data/adb/modules_update/${getModuleIdFromUri(context,flashIt.uri)}/system")
}
else -> return@launch
}
if (!mountNewDirectory.isDirectory && !mountOldDirectory.isDirectory) return@launch
shouldWarningUserMetaModule = true
if (!hasMetaModule() && (flashIt !is FlashIt.FlashModules || flashIt.currentIndex >= flashIt.uris.size - 1)) {
// 如果没有 MetaModule且当前不是多模块刷写或是最后一个需要自动刷写的模块而且有模块需要挂载警告用户
alertDialog.show()
}
}
}
if (flashIt is FlashIt.FlashModules && flashIt.currentIndex < flashIt.uris.size - 1) {
val nextFlashIt = flashIt.copy(
@@ -346,7 +438,13 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
} else if ((isExternalInstall || shouldAutoExit) && flashIt is FlashIt.FlashModules && flashIt.currentIndex >= flashIt.uris.size - 1) {
// 如果是外部安装或需要自动退出且是最后一个模块,安装完成后自动返回
scope.launch {
while (shouldWarningUserMetaModule) {
kotlinx.coroutines.delay(100)
}
kotlinx.coroutines.delay(1000)
while (shouldWarningUserMetaModule) {
kotlinx.coroutines.delay(100)
}
if (shouldAutoExit) {
val sharedPref = context.getSharedPreferences("kernel_flash_prefs", Context.MODE_PRIVATE)
sharedPref.edit { remove("auto_exit_after_flash") }
@@ -356,7 +454,13 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
} else if ((isExternalInstall || shouldAutoExit) && flashIt is FlashIt.FlashModule) {
// 如果是外部安装或需要自动退出的单个模块,安装完成后自动返回
scope.launch {
while (shouldWarningUserMetaModule) {
kotlinx.coroutines.delay(100)
}
kotlinx.coroutines.delay(1000)
while (shouldWarningUserMetaModule) {
kotlinx.coroutines.delay(100)
}
if (shouldAutoExit) {
val sharedPref = context.getSharedPreferences("kernel_flash_prefs", Context.MODE_PRIVATE)
sharedPref.edit { remove("auto_exit_after_flash") }
@@ -705,6 +809,22 @@ suspend fun getModuleNameFromUri(context: Context, uri: Uri): String {
}
}
suspend fun getModuleIdFromUri(context: Context, uri: Uri): String? {
return withContext(Dispatchers.IO) {
try {
if (uri == Uri.EMPTY) {
return@withContext null
}
if (!ModuleUtils.isUriAccessible(context, uri)) {
return@withContext null
}
ModuleUtils.extractModuleId(context, uri)
} catch (_: Exception) {
null
}
}
}
@Parcelize
sealed class FlashIt : Parcelable {
data class FlashBoot(val boot: Uri? = null, val lkm: LkmSelection, val ota: Boolean, val partition: String? = null) : FlashIt()

View File

@@ -182,6 +182,7 @@ fun HomeScreen(navigator: DestinationsNavigator) {
isSimpleMode = viewModel.isSimpleMode,
isHideSusfsStatus = viewModel.isHideSusfsStatus,
isHideZygiskImplement = viewModel.isHideZygiskImplement,
isHideMetaModuleImplement = viewModel.isHideMetaModuleImplement,
showKpmInfo = viewModel.showKpmInfo,
lkmMode = viewModel.systemStatus.lkmMode,
)
@@ -652,6 +653,7 @@ private fun InfoCard(
isSimpleMode: Boolean,
isHideSusfsStatus: Boolean,
isHideZygiskImplement: Boolean,
isHideMetaModuleImplement: Boolean,
showKpmInfo: Boolean,
lkmMode: Boolean?
) {
@@ -784,6 +786,14 @@ private fun InfoCard(
)
}
if (!isHideMetaModuleImplement && !isSimpleMode && systemInfo.metaModuleImplement != "None") {
InfoCardItem(
stringResource(R.string.home_meta_module_implement),
systemInfo.metaModuleImplement,
icon = Icons.Default.Extension,
)
}
if (!isSimpleMode) {
if (lkmMode != true && !showKpmInfo) {
val displayVersion =

View File

@@ -752,6 +752,7 @@ private fun ModuleList(
val uninstall = stringResource(R.string.uninstall)
val cancel = stringResource(android.R.string.cancel)
val moduleUninstallConfirm = stringResource(R.string.module_uninstall_confirm)
val metaModuleUninstallConfirm = stringResource(R.string.metamodule_uninstall_confirm)
val updateText = stringResource(R.string.module_update)
val changelogText = stringResource(R.string.module_changelog)
val downloadingText = stringResource(R.string.module_downloading)
@@ -847,9 +848,10 @@ private fun ModuleList(
suspend fun onModuleUninstallClicked(module: ModuleViewModel.ModuleInfo) {
val isUninstall = !module.remove
if (isUninstall) {
val formatter = if (module.metamodule) metaModuleUninstallConfirm else moduleUninstallConfirm
val confirmResult = confirmDialog.awaitConfirm(
moduleStr,
content = moduleUninstallConfirm.format(module.name),
content = formatter.format(module.name),
confirm = uninstall,
dismiss = cancel
)
@@ -1199,6 +1201,22 @@ fun ModuleItem(
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth()
) {
if (module.metamodule) {
Surface(
shape = RoundedCornerShape(4.dp),
color = MaterialTheme.colorScheme.primary,
modifier = Modifier
) {
Text(
text = "META",
style = MaterialTheme.typography.labelSmall,
modifier = Modifier.padding(horizontal = 4.dp, vertical = 1.dp),
color = MaterialTheme.colorScheme.onPrimary,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
Surface(
shape = RoundedCornerShape(4.dp),
color = MaterialTheme.colorScheme.primary,
@@ -1329,8 +1347,9 @@ fun ModuleItemPreview() {
update = true,
remove = false,
updateJson = "",
hasWebUi = false,
hasActionScript = false,
hasWebUi = true,
hasActionScript = true,
metamodule = true,
dirId = "dirId",
config = ModuleConfig(),
isVerified = true,

View File

@@ -19,8 +19,10 @@ import kotlinx.parcelize.Parcelize
import com.sukisu.ultra.BuildConfig
import com.sukisu.ultra.Natives
import com.sukisu.ultra.ksuApp
import com.topjohnwu.superuser.io.SuFile
import org.json.JSONArray
import java.io.File
import java.util.Properties
/**
@@ -111,6 +113,10 @@ fun install() {
Log.w(TAG, "install result: $result, cost: ${SystemClock.elapsedRealtime() - start}ms")
}
fun hasMetaModule(): Boolean {
return getMetaModuleImplement() != "None"
}
fun listModules(): String {
val shell = getRootShell()
@@ -575,6 +581,26 @@ fun getSuSFSFeatures(): String {
return runCmd(shell, cmd)
}
fun getMetaModuleImplement(): String {
try {
val metaModuleProp = SuFile.open("/data/adb/metamodule/module.prop")
if (!metaModuleProp.isFile) {
Log.i(TAG, "Meta module implement: None")
return "None"
}
val prop = Properties()
prop.load(metaModuleProp.newInputStream())
val name = prop.getProperty("name")
Log.i(TAG, "Meta module implement: $name")
return name
} catch (t : Throwable) {
Log.i(TAG, "Meta module implement: None")
return "None"
}
}
fun getZygiskImplement(): String {
val zygiskModuleIds = listOf(
"zygisksu",

View File

@@ -54,7 +54,8 @@ class HomeViewModel : ViewModel() {
val kpmModuleCount: Int = 0,
val managersList: Natives.ManagersList? = null,
val isDynamicSignEnabled: Boolean = false,
val zygiskImplement: String = ""
val zygiskImplement: String = "",
val metaModuleImplement: String = ""
)
// 状态变量
@@ -79,6 +80,8 @@ class HomeViewModel : ViewModel() {
private set
var isHideZygiskImplement by mutableStateOf(false)
private set
var isHideMetaModuleImplement by mutableStateOf(false)
private set
var isHideLinkCard by mutableStateOf(false)
private set
var showKpmInfo by mutableStateOf(false)
@@ -109,6 +112,7 @@ class HomeViewModel : ViewModel() {
isHideSusfsStatus = settingsPrefs.getBoolean("is_hide_susfs_status", false)
isHideLinkCard = settingsPrefs.getBoolean("is_hide_link_card", false)
isHideZygiskImplement = settingsPrefs.getBoolean("is_hide_zygisk_Implement", false)
isHideMetaModuleImplement = settingsPrefs.getBoolean("is_hide_meta_module_Implement", false)
showKpmInfo = settingsPrefs.getBoolean("show_kpm_info", false)
}
}
@@ -222,7 +226,8 @@ class HomeViewModel : ViewModel() {
superuserCount = moduleInfo.second,
moduleCount = moduleInfo.third,
kpmModuleCount = moduleInfo.fourth,
zygiskImplement = moduleInfo.fifth
zygiskImplement = moduleInfo.fifth,
metaModuleImplement = moduleInfo.sixth
)
}
@@ -398,7 +403,7 @@ class HomeViewModel : ViewModel() {
}
}
private suspend fun loadModuleInfo(): Tuple5<String, Int, Int, Int, String> {
private suspend fun loadModuleInfo(): Tuple6<String, Int, Int, Int, String, String> {
return withContext(Dispatchers.IO) {
val kpmVersion = try {
getKpmVersion()
@@ -430,7 +435,13 @@ class HomeViewModel : ViewModel() {
"None"
}
Tuple5(kpmVersion, superuserCount, moduleCount, kpmModuleCount, zygiskImplement)
val metaModuleImplement = try {
getMetaModuleImplement()
} catch (_: Exception) {
"None"
}
Tuple6(kpmVersion, superuserCount, moduleCount, kpmModuleCount, zygiskImplement, metaModuleImplement)
}
}
@@ -567,6 +578,15 @@ class HomeViewModel : ViewModel() {
}
}
data class Tuple6<T1, T2, T3, T4, T5, T6>(
val first: T1,
val second: T2,
val third: T3,
val fourth: T4,
val fifth: T5,
val sixth: T6
)
data class Tuple5<T1, T2, T3, T4, T5>(
val first: T1,
val second: T2,

View File

@@ -85,6 +85,7 @@ class ModuleViewModel : ViewModel() {
val updateJson: String,
val hasWebUi: Boolean,
val hasActionScript: Boolean,
val metamodule: Boolean,
val dirId: String, // real module id (dir name)
var config: ModuleConfig? = null,
var isVerified: Boolean = false, // 添加验证状态字段
@@ -151,6 +152,7 @@ class ModuleViewModel : ViewModel() {
obj.optString("updateJson"),
obj.getBooleanCompat("web"),
obj.getBooleanCompat("action"),
obj.getBooleanCompat("metamodule"),
obj.optString("dir_id", obj.getString("id"))
)
}.toList()
@@ -305,6 +307,7 @@ fun ModuleViewModel.ModuleInfo.copy(
updateJson: String = this.updateJson,
hasWebUi: Boolean = this.hasWebUi,
hasActionScript: Boolean = this.hasActionScript,
metamodule: Boolean = this.metamodule,
dirId: String = this.dirId,
config: ModuleConfig? = this.config,
isVerified: Boolean = this.isVerified,
@@ -312,7 +315,7 @@ fun ModuleViewModel.ModuleInfo.copy(
): ModuleViewModel.ModuleInfo {
return ModuleViewModel.ModuleInfo(
id, name, author, version, versionCode, description,
enabled, update, remove, updateJson, hasWebUi, hasActionScript,
enabled, update, remove, updateJson, hasWebUi, hasActionScript, metamodule,
dirId, config, isVerified, verificationTimestamp
)
}

View File

@@ -312,6 +312,16 @@ private fun HideOptionsSettings(
onChange = handlers::handleHideZygiskImplementChange
)
// 元模块实现状态信息
SwitchSettingItem(
icon = Icons.Filled.VisibilityOff,
title = stringResource(R.string.hide_meta_module_implement),
summary = stringResource(R.string.hide_meta_module_implement_summary),
checked = state.isHideMetaModuleImplement,
onChange = handlers::handleHideMetaModuleImplementChange
)
// KPM 状态信息隐藏
if (Natives.version >= Natives.MINIMAL_SUPPORTED_KPM) {
SwitchSettingItem(
icon = Icons.Filled.VisibilityOff,

View File

@@ -341,6 +341,14 @@ class MoreSettingsHandlers(
state.isHideZygiskImplement = newValue
}
/**
* 处理隐藏元模块实现变更
*/
fun handleHideMetaModuleImplementChange(newValue: Boolean) {
prefs.edit { putBoolean("is_hide_meta_module_Implement", newValue) }
state.isHideMetaModuleImplement = newValue
}
/**
* 处理隐藏链接卡片变更
*/

View File

@@ -60,6 +60,7 @@ class MoreSettingsState(
var isHideOtherInfo by mutableStateOf(prefs.getBoolean("is_hide_other_info", false))
var isShowKpmInfo by mutableStateOf(prefs.getBoolean("show_kpm_info", false))
var isHideZygiskImplement by mutableStateOf(prefs.getBoolean("is_hide_zygisk_Implement", false))
var isHideMetaModuleImplement by mutableStateOf(prefs.getBoolean("is_hide_meta_module_Implement", false))
var isHideSusfsStatus by mutableStateOf(prefs.getBoolean("is_hide_susfs_status", false))
var isHideLinkCard by mutableStateOf(prefs.getBoolean("is_hide_link_card", false))
var isHideTagRow by mutableStateOf(prefs.getBoolean("is_hide_tag_row", false))