diff --git a/manager/app/src/main/AndroidManifest.xml b/manager/app/src/main/AndroidManifest.xml
index e5903da2..15841516 100644
--- a/manager/app/src/main/AndroidManifest.xml
+++ b/manager/app/src/main/AndroidManifest.xml
@@ -35,17 +35,20 @@
+
+
+
diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/MainActivity.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/MainActivity.kt
index b50699d3..eaea75c8 100644
--- a/manager/app/src/main/java/com/sukisu/ultra/ui/MainActivity.kt
+++ b/manager/app/src/main/java/com/sukisu/ultra/ui/MainActivity.kt
@@ -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>(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(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,104 +236,55 @@ class MainActivity : ComponentActivity() {
}
}
- private enum class ZipType {
- MODULE,
- KERNEL,
- UNKNOWN
- }
+ private suspend fun detectZipTypeAndShowConfirmation(zipUris: ArrayList) {
+ 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
- }
- }
-
- zipStream.closeEntry()
- entry = zipStream.nextEntry
- }
-
- when {
- hasModuleProp -> ZipType.MODULE
- hasToolsFolder && hasAnykernelSh -> ZipType.KERNEL
- else -> ZipType.UNKNOWN
- }
+ withContext(Dispatchers.Main) {
+ if (zipFileInfos.isNotEmpty()) {
+ pendingZipFiles.value = zipFileInfos
+ showConfirmationDialog.value = true
+ } else {
+ finish()
}
- } ?: ZipType.UNKNOWN
- } catch (e: IOException) {
+ }
+ } catch (e: Exception) {
+ withContext(Dispatchers.Main) {
+ finish()
+ }
e.printStackTrace()
- ZipType.UNKNOWN
}
}
- private suspend fun detectZipTypeAndNavigate(
- zipUris: ArrayList,
+ private fun navigateToFlashScreen(
+ zipFiles: List,
navigator: com.ramcosta.composedestinations.navigation.DestinationsNavigator
) {
- withContext(Dispatchers.IO) {
- try {
- val moduleUris = mutableListOf()
- val kernelUris = mutableListOf()
+ 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() -> {
- if (kernelUris.size == 1 && rootAvailable()) {
- navigator.navigate(
- InstallScreenDestination(
- preselectedKernelUri = kernelUris.first().toString()
- )
- )
- }
- setAutoExitAfterFlash()
- }
- // 模块文件
- moduleUris.isNotEmpty() -> {
- navigator.navigate(
- FlashScreenDestination(
- FlashIt.FlashModules(ArrayList(moduleUris))
- )
+ when {
+ // 内核文件
+ kernelUris.isNotEmpty() && moduleUris.isEmpty() -> {
+ if (kernelUris.size == 1 && rootAvailable()) {
+ navigator.navigate(
+ InstallScreenDestination(
+ preselectedKernelUri = kernelUris.first().toString()
)
- 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()
}
}
}
diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/InstallConfirmationDialog.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/InstallConfirmationDialog.kt
new file mode 100644
index 00000000..6ae6a475
--- /dev/null
+++ b/manager/app/src/main/java/com/sukisu/ultra/ui/component/InstallConfirmationDialog.kt
@@ -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()
+
+ 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()
+
+ 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): List {
+ return withContext(Dispatchers.IO) {
+ val zipFileInfos = mutableListOf()
+
+ 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,
+ onConfirm: (List) -> 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)
+ )
+ }
+}
\ No newline at end of file
diff --git a/manager/app/src/main/res/values-zh-rCN/strings.xml b/manager/app/src/main/res/values-zh-rCN/strings.xml
index f05f6cbf..87aa8db2 100644
--- a/manager/app/src/main/res/values-zh-rCN/strings.xml
+++ b/manager/app/src/main/res/values-zh-rCN/strings.xml
@@ -657,4 +657,17 @@
您确定要清理运行环境吗?这将停止扫描服务并删除相关文件
运行环境清理成功
运行环境清理失败
+
+ 确认安装
+ 确认安装(%d 个文件)
+ 确认安装
+ 模块
+ 内核
+ 未知类型
+ 未知内核
+ 未知文件
+ 版本
+ 作者
+ 描述
+ 支持设备
diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml
index e8cdabf7..2477f81c 100644
--- a/manager/app/src/main/res/values/strings.xml
+++ b/manager/app/src/main/res/values/strings.xml
@@ -665,4 +665,17 @@ Important Note:\n
Are you sure you want to clean the runtime environment? This will stop the scanner service and remove related files.
Runtime environment cleaned successfully
Failed to clean runtime environment
+
+ Confirm Installation
+ Confirm Installation (%d files)
+ Install
+ Module
+ Kernel
+ Unknown
+ Unknown Kernel
+ Unknown File
+ Version
+ Author
+ Description
+ Supported Devices
\ No newline at end of file