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:
@@ -35,17 +35,20 @@
|
|||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
<data android:mimeType="application/zip" />
|
<data android:mimeType="application/zip" />
|
||||||
|
<data android:mimeType="application/vnd.android.package-archive" />
|
||||||
<data android:scheme="content" />
|
<data android:scheme="content" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND" />
|
<action android:name="android.intent.action.SEND" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<data android:mimeType="application/zip" />
|
<data android:mimeType="application/zip" />
|
||||||
|
<data android:mimeType="application/vnd.android.package-archive" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<data android:mimeType="application/zip" />
|
<data android:mimeType="application/zip" />
|
||||||
|
<data android:mimeType="application/vnd.android.package-archive" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
|||||||
@@ -11,13 +11,10 @@ import androidx.activity.compose.setContent
|
|||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.compose.animation.*
|
import androidx.compose.animation.*
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation.NavBackStackEntry
|
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.viewmodel.SuperUserViewModel
|
||||||
import com.sukisu.ultra.ui.webui.initPlatform
|
import com.sukisu.ultra.ui.webui.initPlatform
|
||||||
import com.sukisu.ultra.ui.screen.FlashIt
|
import com.sukisu.ultra.ui.screen.FlashIt
|
||||||
|
import com.sukisu.ultra.ui.component.*
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import zako.zako.zako.zakoui.activity.component.BottomBar
|
import zako.zako.zako.zakoui.activity.component.BottomBar
|
||||||
import zako.zako.zako.zakoui.activity.util.*
|
import zako.zako.zako.zakoui.activity.util.*
|
||||||
import java.util.zip.ZipInputStream
|
|
||||||
import java.io.IOException
|
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import com.sukisu.ultra.ui.util.rootAvailable
|
import com.sukisu.ultra.ui.util.rootAvailable
|
||||||
|
|
||||||
@@ -61,6 +57,9 @@ class MainActivity : ComponentActivity() {
|
|||||||
val showKpmInfo: Boolean = false
|
val showKpmInfo: Boolean = false
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private var showConfirmationDialog = mutableStateOf(false)
|
||||||
|
private var pendingZipFiles = mutableStateOf<List<ZipFileInfo>>(emptyList())
|
||||||
|
|
||||||
private lateinit var themeChangeObserver: ThemeChangeContentObserver
|
private lateinit var themeChangeObserver: ThemeChangeContentObserver
|
||||||
|
|
||||||
// 添加标记避免重复初始化
|
// 添加标记避免重复初始化
|
||||||
@@ -102,7 +101,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)
|
intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)
|
||||||
} else {
|
} else {
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)
|
intent.getParcelableExtra(Intent.EXTRA_STREAM)
|
||||||
}
|
}
|
||||||
uri?.let { arrayListOf(it) }
|
uri?.let { arrayListOf(it) }
|
||||||
}
|
}
|
||||||
@@ -140,10 +139,24 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
val navigator = navController.rememberDestinationsNavigator()
|
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) {
|
LaunchedEffect(zipUri) {
|
||||||
if (!zipUri.isNullOrEmpty()) {
|
if (!zipUri.isNullOrEmpty()) {
|
||||||
// 检测 ZIP 文件类型并导航到相应界面
|
// 检测 ZIP 文件类型并显示确认对话框
|
||||||
detectZipTypeAndNavigate(zipUri, navigator)
|
detectZipTypeAndShowConfirmation(zipUri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,104 +236,55 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum class ZipType {
|
private suspend fun detectZipTypeAndShowConfirmation(zipUris: ArrayList<Uri>) {
|
||||||
MODULE,
|
try {
|
||||||
KERNEL,
|
val zipFileInfos = ZipFileDetector.detectAndParseZipFiles(this, zipUris)
|
||||||
UNKNOWN
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun detectZipType(uri: Uri): ZipType {
|
withContext(Dispatchers.Main) {
|
||||||
return try {
|
if (zipFileInfos.isNotEmpty()) {
|
||||||
contentResolver.openInputStream(uri)?.use { inputStream ->
|
pendingZipFiles.value = zipFileInfos
|
||||||
ZipInputStream(inputStream).use { zipStream ->
|
showConfirmationDialog.value = true
|
||||||
var hasModuleProp = false
|
} else {
|
||||||
var hasToolsFolder = false
|
finish()
|
||||||
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) {
|
} catch (e: Exception) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
ZipType.UNKNOWN
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun detectZipTypeAndNavigate(
|
private fun navigateToFlashScreen(
|
||||||
zipUris: ArrayList<Uri>,
|
zipFiles: List<ZipFileInfo>,
|
||||||
navigator: com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
navigator: com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
) {
|
) {
|
||||||
withContext(Dispatchers.IO) {
|
lifecycleScope.launch {
|
||||||
try {
|
val moduleUris = zipFiles.filter { it.type == ZipType.MODULE }.map { it.uri }
|
||||||
val moduleUris = mutableListOf<Uri>()
|
val kernelUris = zipFiles.filter { it.type == ZipType.KERNEL }.map { it.uri }
|
||||||
val kernelUris = mutableListOf<Uri>()
|
|
||||||
|
|
||||||
for (uri in zipUris) {
|
when {
|
||||||
val zipType = detectZipType(uri)
|
// 内核文件
|
||||||
when (zipType) {
|
kernelUris.isNotEmpty() && moduleUris.isEmpty() -> {
|
||||||
ZipType.MODULE -> moduleUris.add(uri)
|
if (kernelUris.size == 1 && rootAvailable()) {
|
||||||
ZipType.KERNEL -> kernelUris.add(uri)
|
navigator.navigate(
|
||||||
ZipType.UNKNOWN -> {
|
InstallScreenDestination(
|
||||||
}
|
preselectedKernelUri = kernelUris.first().toString()
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据检测结果导航
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
when {
|
|
||||||
// 内核文件
|
|
||||||
kernelUris.isNotEmpty() && moduleUris.isEmpty() -> {
|
|
||||||
if (kernelUris.size == 1 && rootAvailable()) {
|
|
||||||
navigator.navigate(
|
|
||||||
InstallScreenDestination(
|
|
||||||
preselectedKernelUri = kernelUris.first().toString()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
setAutoExitAfterFlash()
|
|
||||||
}
|
|
||||||
// 模块文件
|
|
||||||
moduleUris.isNotEmpty() -> {
|
|
||||||
navigator.navigate(
|
|
||||||
FlashScreenDestination(
|
|
||||||
FlashIt.FlashModules(ArrayList(moduleUris))
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
setAutoExitAfterFlash()
|
)
|
||||||
}
|
|
||||||
// 如果没有识别出任何类型的文件,则直接退出
|
|
||||||
else -> {
|
|
||||||
(this@MainActivity as? ComponentActivity)?.finish()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
setAutoExitAfterFlash()
|
||||||
|
}
|
||||||
|
// 模块文件
|
||||||
|
moduleUris.isNotEmpty() -> {
|
||||||
|
navigator.navigate(
|
||||||
|
FlashScreenDestination(
|
||||||
|
FlashIt.FlashModules(ArrayList(moduleUris))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
setAutoExitAfterFlash()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
(this@MainActivity as? ComponentActivity)?.finish()
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -657,4 +657,17 @@
|
|||||||
<string name="clean_runtime_environment_confirm">您确定要清理运行环境吗?这将停止扫描服务并删除相关文件</string>
|
<string name="clean_runtime_environment_confirm">您确定要清理运行环境吗?这将停止扫描服务并删除相关文件</string>
|
||||||
<string name="clean_runtime_environment_success">运行环境清理成功</string>
|
<string name="clean_runtime_environment_success">运行环境清理成功</string>
|
||||||
<string name="clean_runtime_environment_failed">运行环境清理失败</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>
|
</resources>
|
||||||
|
|||||||
@@ -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_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_success">Runtime environment cleaned successfully</string>
|
||||||
<string name="clean_runtime_environment_failed">Failed to clean runtime environment</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>
|
</resources>
|
||||||
Reference in New Issue
Block a user