manager: Add detailed information about the module / anykernel3 compressed package from sharing and direct flashing

- Treat certain XP module APK files as modules for processing
This commit is contained in:
ShirkNeko
2025-10-11 17:49:36 +08:00
parent fcb7c3e99d
commit 4c512dc7ff
5 changed files with 532 additions and 98 deletions

View File

@@ -35,17 +35,20 @@
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:mimeType="application/zip" />
<data android:mimeType="application/vnd.android.package-archive" />
<data android:scheme="content" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/zip" />
<data android:mimeType="application/vnd.android.package-archive" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/zip" />
<data android:mimeType="application/vnd.android.package-archive" />
</intent-filter>
</activity>

View File

@@ -11,13 +11,10 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.animation.*
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavBackStackEntry
@@ -40,14 +37,13 @@ import com.sukisu.ultra.ui.viewmodel.HomeViewModel
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
import com.sukisu.ultra.ui.webui.initPlatform
import com.sukisu.ultra.ui.screen.FlashIt
import com.sukisu.ultra.ui.component.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import zako.zako.zako.zakoui.activity.component.BottomBar
import zako.zako.zako.zakoui.activity.util.*
import java.util.zip.ZipInputStream
import java.io.IOException
import androidx.core.content.edit
import com.sukisu.ultra.ui.util.rootAvailable
@@ -61,6 +57,9 @@ class MainActivity : ComponentActivity() {
val showKpmInfo: Boolean = false
)
private var showConfirmationDialog = mutableStateOf(false)
private var pendingZipFiles = mutableStateOf<List<ZipFileInfo>>(emptyList())
private lateinit var themeChangeObserver: ThemeChangeContentObserver
// 添加标记避免重复初始化
@@ -102,7 +101,7 @@ class MainActivity : ComponentActivity() {
intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)
intent.getParcelableExtra(Intent.EXTRA_STREAM)
}
uri?.let { arrayListOf(it) }
}
@@ -140,10 +139,24 @@ class MainActivity : ComponentActivity() {
val navigator = navController.rememberDestinationsNavigator()
InstallConfirmationDialog(
show = showConfirmationDialog.value,
zipFiles = pendingZipFiles.value,
onConfirm = { confirmedFiles ->
showConfirmationDialog.value = false
navigateToFlashScreen(confirmedFiles, navigator)
},
onDismiss = {
showConfirmationDialog.value = false
pendingZipFiles.value = emptyList()
finish()
}
)
LaunchedEffect(zipUri) {
if (!zipUri.isNullOrEmpty()) {
// 检测 ZIP 文件类型并导航到相应界面
detectZipTypeAndNavigate(zipUri, navigator)
// 检测 ZIP 文件类型并显示确认对话框
detectZipTypeAndShowConfirmation(zipUri)
}
}
@@ -223,74 +236,34 @@ class MainActivity : ComponentActivity() {
}
}
private enum class ZipType {
MODULE,
KERNEL,
UNKNOWN
}
private suspend fun detectZipTypeAndShowConfirmation(zipUris: ArrayList<Uri>) {
try {
val zipFileInfos = ZipFileDetector.detectAndParseZipFiles(this, zipUris)
private fun detectZipType(uri: Uri): ZipType {
return try {
contentResolver.openInputStream(uri)?.use { inputStream ->
ZipInputStream(inputStream).use { zipStream ->
var hasModuleProp = false
var hasToolsFolder = false
var hasAnykernelSh = false
var entry = zipStream.nextEntry
while (entry != null) {
val entryName = entry.name.lowercase()
when {
entryName == "module.prop" || entryName.endsWith("/module.prop") -> {
hasModuleProp = true
}
entryName.startsWith("tools/") || entryName == "tools" -> {
hasToolsFolder = true
}
entryName == "anykernel.sh" || entryName.endsWith("/anykernel.sh") -> {
hasAnykernelSh = true
withContext(Dispatchers.Main) {
if (zipFileInfos.isNotEmpty()) {
pendingZipFiles.value = zipFileInfos
showConfirmationDialog.value = true
} else {
finish()
}
}
zipStream.closeEntry()
entry = zipStream.nextEntry
} catch (e: Exception) {
withContext(Dispatchers.Main) {
finish()
}
when {
hasModuleProp -> ZipType.MODULE
hasToolsFolder && hasAnykernelSh -> ZipType.KERNEL
else -> ZipType.UNKNOWN
}
}
} ?: ZipType.UNKNOWN
} catch (e: IOException) {
e.printStackTrace()
ZipType.UNKNOWN
}
}
private suspend fun detectZipTypeAndNavigate(
zipUris: ArrayList<Uri>,
private fun navigateToFlashScreen(
zipFiles: List<ZipFileInfo>,
navigator: com.ramcosta.composedestinations.navigation.DestinationsNavigator
) {
withContext(Dispatchers.IO) {
try {
val moduleUris = mutableListOf<Uri>()
val kernelUris = mutableListOf<Uri>()
lifecycleScope.launch {
val moduleUris = zipFiles.filter { it.type == ZipType.MODULE }.map { it.uri }
val kernelUris = zipFiles.filter { it.type == ZipType.KERNEL }.map { it.uri }
for (uri in zipUris) {
val zipType = detectZipType(uri)
when (zipType) {
ZipType.MODULE -> moduleUris.add(uri)
ZipType.KERNEL -> kernelUris.add(uri)
ZipType.UNKNOWN -> {
}
}
}
// 根据检测结果导航
withContext(Dispatchers.Main) {
when {
// 内核文件
kernelUris.isNotEmpty() && moduleUris.isEmpty() -> {
@@ -312,15 +285,6 @@ class MainActivity : ComponentActivity() {
)
setAutoExitAfterFlash()
}
// 如果没有识别出任何类型的文件,则直接退出
else -> {
(this@MainActivity as? ComponentActivity)?.finish()
}
}
}
} catch (e: Exception) {
(this@MainActivity as? ComponentActivity)?.finish()
e.printStackTrace()
}
}
}

View File

@@ -0,0 +1,441 @@
package com.sukisu.ultra.ui.component
import android.content.Context
import android.net.Uri
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Help
import androidx.compose.material.icons.filled.Extension
import androidx.compose.material.icons.filled.GetApp
import androidx.compose.material.icons.filled.Memory
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.sukisu.ultra.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStreamReader
import java.util.zip.ZipInputStream
enum class ZipType {
MODULE,
KERNEL,
UNKNOWN
}
data class ZipFileInfo(
val uri: Uri,
val type: ZipType,
val name: String = "",
val version: String = "",
val versionCode: String = "",
val author: String = "",
val description: String = "",
val kernelVersion: String = "",
val supported: String = ""
)
object ZipFileDetector {
fun detectZipType(context: Context, uri: Uri): ZipType {
return try {
context.contentResolver.openInputStream(uri)?.use { inputStream ->
ZipInputStream(inputStream).use { zipStream ->
var hasModuleProp = false
var hasToolsFolder = false
var hasAnykernelSh = false
var entry = zipStream.nextEntry
while (entry != null) {
val entryName = entry.name.lowercase()
when {
entryName == "module.prop" || entryName.endsWith("/module.prop") -> {
hasModuleProp = true
}
entryName.startsWith("tools/") || entryName == "tools" -> {
hasToolsFolder = true
}
entryName == "anykernel.sh" || entryName.endsWith("/anykernel.sh") -> {
hasAnykernelSh = true
}
}
zipStream.closeEntry()
entry = zipStream.nextEntry
}
when {
hasModuleProp -> ZipType.MODULE
hasToolsFolder && hasAnykernelSh -> ZipType.KERNEL
else -> ZipType.UNKNOWN
}
}
} ?: ZipType.UNKNOWN
} catch (e: IOException) {
e.printStackTrace()
ZipType.UNKNOWN
}
}
fun parseModuleInfo(context: Context, uri: Uri): ZipFileInfo {
var zipInfo = ZipFileInfo(uri = uri, type = ZipType.MODULE)
try {
context.contentResolver.openInputStream(uri)?.use { inputStream ->
ZipInputStream(inputStream).use { zipStream ->
var entry = zipStream.nextEntry
while (entry != null) {
if (entry.name.lowercase() == "module.prop" || entry.name.endsWith("/module.prop")) {
val reader = BufferedReader(InputStreamReader(zipStream))
val props = mutableMapOf<String, String>()
var line = reader.readLine()
while (line != null) {
if (line.contains("=") && !line.startsWith("#")) {
val parts = line.split("=", limit = 2)
if (parts.size == 2) {
props[parts[0].trim()] = parts[1].trim()
}
}
line = reader.readLine()
}
zipInfo = zipInfo.copy(
name = props["name"] ?: context.getString(R.string.unknown_module),
version = props["version"] ?: "",
versionCode = props["versionCode"] ?: "",
author = props["author"] ?: "",
description = props["description"] ?: ""
)
break
}
zipStream.closeEntry()
entry = zipStream.nextEntry
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
return zipInfo
}
fun parseKernelInfo(context: Context, uri: Uri): ZipFileInfo {
var zipInfo = ZipFileInfo(uri = uri, type = ZipType.KERNEL)
try {
context.contentResolver.openInputStream(uri)?.use { inputStream ->
ZipInputStream(inputStream).use { zipStream ->
var entry = zipStream.nextEntry
while (entry != null) {
if (entry.name.lowercase() == "anykernel.sh" || entry.name.endsWith("/anykernel.sh")) {
val reader = BufferedReader(InputStreamReader(zipStream))
val props = mutableMapOf<String, String>()
var inPropertiesBlock = false
var line = reader.readLine()
while (line != null) {
if (line.contains("properties()")) {
inPropertiesBlock = true
} else if (inPropertiesBlock && line.contains("'; }")) {
inPropertiesBlock = false
} else if (inPropertiesBlock) {
val propertyLine = line.trim()
if (propertyLine.contains("=") && !propertyLine.startsWith("#")) {
val parts = propertyLine.split("=", limit = 2)
if (parts.size == 2) {
val key = parts[0].trim()
val value = parts[1].trim().removeSurrounding("'").removeSurrounding("\"")
when (key) {
"kernel.string" -> props["name"] = value
"supported.versions" -> props["supported"] = value
}
}
}
}
// 解析普通变量定义
if (line.contains("kernel.string=") && !inPropertiesBlock) {
val value = line.substringAfter("kernel.string=").trim().removeSurrounding("\"")
props["name"] = value
}
if (line.contains("supported.versions=") && !inPropertiesBlock) {
val value = line.substringAfter("supported.versions=").trim().removeSurrounding("\"")
props["supported"] = value
}
if (line.contains("kernel.version=") && !inPropertiesBlock) {
val value = line.substringAfter("kernel.version=").trim().removeSurrounding("\"")
props["version"] = value
}
if (line.contains("kernel.author=") && !inPropertiesBlock) {
val value = line.substringAfter("kernel.author=").trim().removeSurrounding("\"")
props["author"] = value
}
line = reader.readLine()
}
zipInfo = zipInfo.copy(
name = props["name"] ?: context.getString(R.string.unknown_kernel),
version = props["version"] ?: "",
author = props["author"] ?: "",
supported = props["supported"] ?: "",
kernelVersion = props["version"] ?: ""
)
break
}
zipStream.closeEntry()
entry = zipStream.nextEntry
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
return zipInfo
}
suspend fun detectAndParseZipFiles(context: Context, zipUris: List<Uri>): List<ZipFileInfo> {
return withContext(Dispatchers.IO) {
val zipFileInfos = mutableListOf<ZipFileInfo>()
for (uri in zipUris) {
val zipType = detectZipType(context, uri)
val zipInfo = when (zipType) {
ZipType.MODULE -> parseModuleInfo(context, uri)
ZipType.KERNEL -> parseKernelInfo(context, uri)
ZipType.UNKNOWN -> ZipFileInfo(
uri = uri,
type = ZipType.UNKNOWN,
name = context.getString(R.string.unknown_file)
)
}
zipFileInfos.add(zipInfo)
}
zipFileInfos.filter { it.type != ZipType.UNKNOWN }
}
}
}
@Composable
fun InstallConfirmationDialog(
show: Boolean,
zipFiles: List<ZipFileInfo>,
onConfirm: (List<ZipFileInfo>) -> Unit,
onDismiss: () -> Unit
) {
if (show && zipFiles.isNotEmpty()) {
val context = LocalContext.current
AlertDialog(
onDismissRequest = onDismiss,
title = {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Icon(
imageVector = if (zipFiles.any { it.type == ZipType.KERNEL })
Icons.Default.Memory else Icons.Default.Extension,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = if (zipFiles.size == 1) {
context.getString(R.string.confirm_installation)
} else {
context.getString(R.string.confirm_multiple_installation, zipFiles.size)
},
style = MaterialTheme.typography.headlineSmall
)
}
},
text = {
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.heightIn(max = 400.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(zipFiles.size) { index ->
val zipFile = zipFiles[index]
InstallItemCard(zipFile = zipFile)
}
}
},
confirmButton = {
Button(
onClick = { onConfirm(zipFiles) },
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary
)
) {
Icon(
imageVector = Icons.Default.GetApp,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(context.getString(R.string.install_confirm))
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text(
context.getString(android.R.string.cancel),
color = MaterialTheme.colorScheme.onSurface
)
}
},
modifier = Modifier.widthIn(min = 320.dp, max = 560.dp)
)
}
}
@Composable
fun InstallItemCard(zipFile: ZipFileInfo) {
val context = LocalContext.current
ElevatedCard(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.elevatedCardColors(
containerColor = when (zipFile.type) {
ZipType.MODULE -> MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
ZipType.KERNEL -> MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.3f)
else -> MaterialTheme.colorScheme.surfaceVariant
}
),
elevation = CardDefaults.elevatedCardElevation(defaultElevation = 0.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Icon(
imageVector = when (zipFile.type) {
ZipType.MODULE -> Icons.Default.Extension
ZipType.KERNEL -> Icons.Default.Memory
else -> Icons.AutoMirrored.Filled.Help
},
contentDescription = null,
tint = when (zipFile.type) {
ZipType.MODULE -> MaterialTheme.colorScheme.primary
ZipType.KERNEL -> MaterialTheme.colorScheme.tertiary
else -> MaterialTheme.colorScheme.onSurfaceVariant
},
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = zipFile.name.ifEmpty {
when (zipFile.type) {
ZipType.MODULE -> context.getString(R.string.unknown_module)
ZipType.KERNEL -> context.getString(R.string.unknown_kernel)
else -> context.getString(R.string.unknown_file)
}
},
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface
)
Text(
text = when (zipFile.type) {
ZipType.MODULE -> context.getString(R.string.module_package)
ZipType.KERNEL -> context.getString(R.string.kernel_package)
else -> context.getString(R.string.unknown_package)
},
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
// 详细信息
if (zipFile.version.isNotEmpty() || zipFile.author.isNotEmpty() ||
zipFile.description.isNotEmpty() || zipFile.supported.isNotEmpty()) {
Spacer(modifier = Modifier.height(12.dp))
HorizontalDivider(
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f),
thickness = 0.5.dp
)
Spacer(modifier = Modifier.height(8.dp))
// 版本信息
if (zipFile.version.isNotEmpty()) {
InfoRow(
label = context.getString(R.string.version),
value = zipFile.version + if (zipFile.versionCode.isNotEmpty()) " (${zipFile.versionCode})" else ""
)
}
// 作者信息
if (zipFile.author.isNotEmpty()) {
InfoRow(
label = context.getString(R.string.author),
value = zipFile.author
)
}
// 描述信息 (仅模块)
if (zipFile.description.isNotEmpty() && zipFile.type == ZipType.MODULE) {
InfoRow(
label = context.getString(R.string.description),
value = zipFile.description
)
}
// 支持设备 (仅内核)
if (zipFile.supported.isNotEmpty() && zipFile.type == ZipType.KERNEL) {
InfoRow(
label = context.getString(R.string.supported_devices),
value = zipFile.supported
)
}
}
}
}
}
@Composable
fun InfoRow(label: String, value: String) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 2.dp),
verticalAlignment = Alignment.Top
) {
Text(
text = "$label:",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.widthIn(min = 60.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = value,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.weight(1f)
)
}
}

View File

@@ -657,4 +657,17 @@
<string name="clean_runtime_environment_confirm">您确定要清理运行环境吗?这将停止扫描服务并删除相关文件</string>
<string name="clean_runtime_environment_success">运行环境清理成功</string>
<string name="clean_runtime_environment_failed">运行环境清理失败</string>
<!-- 确认安装相关字符串 -->
<string name="confirm_installation">确认安装</string>
<string name="confirm_multiple_installation">确认安装(%d 个文件)</string>
<string name="install_confirm">确认安装</string>
<string name="module_package">模块</string>
<string name="kernel_package">内核</string>
<string name="unknown_package">未知类型</string>
<string name="unknown_kernel">未知内核</string>
<string name="unknown_file">未知文件</string>
<string name="version">版本</string>
<string name="author">作者</string>
<string name="description">描述</string>
<string name="supported_devices">支持设备</string>
</resources>

View File

@@ -665,4 +665,17 @@ Important Note:\n
<string name="clean_runtime_environment_confirm">Are you sure you want to clean the runtime environment? This will stop the scanner service and remove related files.</string>
<string name="clean_runtime_environment_success">Runtime environment cleaned successfully</string>
<string name="clean_runtime_environment_failed">Failed to clean runtime environment</string>
<!-- 确认安装相关字符串 -->
<string name="confirm_installation">Confirm Installation</string>
<string name="confirm_multiple_installation">Confirm Installation (%d files)</string>
<string name="install_confirm">Install</string>
<string name="module_package">Module</string>
<string name="kernel_package">Kernel</string>
<string name="unknown_package">Unknown</string>
<string name="unknown_kernel">Unknown Kernel</string>
<string name="unknown_file">Unknown File</string>
<string name="version">Version</string>
<string name="author">Author</string>
<string name="description">Description</string>
<string name="supported_devices">Supported Devices</string>
</resources>