Manager: Added KPM patching functionality support. close #372

- Integrated KPM patching logic into KernelFlash and display KPM patching status.
- Updated Install to support KPM patching options.
- Implemented local and remote downloads for KPM tools.

Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
This commit is contained in:
ShirkNeko
2025-09-10 19:54:33 +08:00
parent 3ed1d9aebc
commit cd8b6ab382
8 changed files with 864 additions and 16 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,364 @@
package com.sukisu.ultra.network
import android.content.Context
import android.util.Log
import kotlinx.coroutines.*
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.net.HttpURLConnection
import java.net.SocketTimeoutException
import java.net.URL
import java.util.concurrent.TimeUnit
class RemoteToolsDownloader(
private val context: Context,
private val workDir: String
) {
companion object {
private const val TAG = "RemoteToolsDownloader"
// 远程下载URL配置
private const val KPTOOLS_REMOTE_URL = "https://raw.githubusercontent.com/ShirkNeko/SukiSU_patch/refs/heads/main/kpm/kptools"
private const val KPIMG_REMOTE_URL = "https://raw.githubusercontent.com/ShirkNeko/SukiSU_patch/refs/heads/main/kpm/kpimg"
// 网络超时配置(毫秒)
private const val CONNECTION_TIMEOUT = 15000 // 15秒连接超时
private const val READ_TIMEOUT = 30000 // 30秒读取超时
// 最大重试次数
private const val MAX_RETRY_COUNT = 3
// 文件校验相关
private const val MIN_FILE_SIZE = 1024
}
interface DownloadProgressListener {
fun onProgress(fileName: String, progress: Int, total: Int)
fun onLog(message: String)
fun onError(fileName: String, error: String)
fun onSuccess(fileName: String, isRemote: Boolean)
}
data class DownloadResult(
val success: Boolean,
val isRemoteSource: Boolean,
val errorMessage: String? = null
)
suspend fun downloadToolsAsync(listener: DownloadProgressListener?): Map<String, DownloadResult> = withContext(Dispatchers.IO) {
val results = mutableMapOf<String, DownloadResult>()
listener?.onLog("Starting to prepare KPM tool files...")
try {
// 确保工作目录存在
File(workDir).mkdirs()
// 并行下载两个工具文件
val kptoolsDeferred = async { downloadSingleTool("kptools", KPTOOLS_REMOTE_URL, listener) }
val kpimgDeferred = async { downloadSingleTool("kpimg", KPIMG_REMOTE_URL, listener) }
// 等待所有下载完成
results["kptools"] = kptoolsDeferred.await()
results["kpimg"] = kpimgDeferred.await()
// 检查kptools执行权限
val kptoolsFile = File(workDir, "kptools")
if (kptoolsFile.exists()) {
setExecutablePermission(kptoolsFile.absolutePath)
listener?.onLog("Set kptools execution permission")
}
val successCount = results.values.count { it.success }
val remoteCount = results.values.count { it.success && it.isRemoteSource }
listener?.onLog("KPM tools preparation completed: Success $successCount/2, Remote downloaded $remoteCount")
} catch (e: Exception) {
Log.e(TAG, "Exception occurred while downloading tools", e)
listener?.onLog("Exception occurred during tool download: ${e.message}")
if (!results.containsKey("kptools")) {
results["kptools"] = downloadSingleTool("kptools", null, listener)
}
if (!results.containsKey("kpimg")) {
results["kpimg"] = downloadSingleTool("kpimg", null, listener)
}
}
results.toMap()
}
private suspend fun downloadSingleTool(
fileName: String,
remoteUrl: String?,
listener: DownloadProgressListener?
): DownloadResult = withContext(Dispatchers.IO) {
val targetFile = File(workDir, fileName)
if (remoteUrl == null) {
return@withContext useLocalVersion(fileName, targetFile, listener)
}
// 尝试从远程下载
listener?.onLog("Downloading $fileName from remote repository...")
var lastError = ""
// 重试机制
repeat(MAX_RETRY_COUNT) { attempt ->
try {
val result = downloadFromRemote(fileName, remoteUrl, targetFile, listener)
if (result.success) {
listener?.onSuccess(fileName, true)
return@withContext result
}
lastError = result.errorMessage ?: "Unknown error"
} catch (e: Exception) {
lastError = e.message ?: "Network exception"
Log.w(TAG, "$fileName download attempt ${attempt + 1} failed", e)
if (attempt < MAX_RETRY_COUNT - 1) {
listener?.onLog("$fileName download failed, retrying in ${(attempt + 1) * 2} seconds...")
delay(TimeUnit.SECONDS.toMillis((attempt + 1) * 2L))
}
}
}
// 所有重试都失败,回退到本地版本
listener?.onError(fileName, "Remote download failed: $lastError")
listener?.onLog("$fileName remote download failed, falling back to local version...")
useLocalVersion(fileName, targetFile, listener)
}
private suspend fun downloadFromRemote(
fileName: String,
remoteUrl: String,
targetFile: File,
listener: DownloadProgressListener?
): DownloadResult = withContext(Dispatchers.IO) {
var connection: HttpURLConnection? = null
try {
val url = URL(remoteUrl)
connection = url.openConnection() as HttpURLConnection
// 设置连接参数
connection.apply {
connectTimeout = CONNECTION_TIMEOUT
readTimeout = READ_TIMEOUT
requestMethod = "GET"
setRequestProperty("User-Agent", "SukiSU-KPM-Downloader/1.0")
setRequestProperty("Accept", "*/*")
setRequestProperty("Connection", "close")
}
// 建立连接
connection.connect()
val responseCode = connection.responseCode
if (responseCode != HttpURLConnection.HTTP_OK) {
return@withContext DownloadResult(
false,
isRemoteSource = false,
errorMessage = "HTTP error code: $responseCode"
)
}
val fileLength = connection.contentLength
Log.d(TAG, "$fileName remote file size: $fileLength bytes")
// 创建临时文件
val tempFile = File(targetFile.absolutePath + ".tmp")
// 下载文件
connection.inputStream.use { input ->
FileOutputStream(tempFile).use { output ->
val buffer = ByteArray(8192)
var totalBytes = 0
var bytesRead: Int
while (input.read(buffer).also { bytesRead = it } != -1) {
// 检查协程是否被取消
ensureActive()
output.write(buffer, 0, bytesRead)
totalBytes += bytesRead
// 更新下载进度
if (fileLength > 0) {
listener?.onProgress(fileName, totalBytes, fileLength)
}
}
output.flush()
}
}
// 验证下载的文件
if (!validateDownloadedFile(tempFile, fileName)) {
tempFile.delete()
return@withContext DownloadResult(
success = false,
isRemoteSource = false,
errorMessage = "File verification failed"
)
}
// 移动临时文件到目标位置
if (targetFile.exists()) {
targetFile.delete()
}
if (!tempFile.renameTo(targetFile)) {
tempFile.delete()
return@withContext DownloadResult(
false,
isRemoteSource = false,
errorMessage = "Failed to move file"
)
}
Log.i(TAG, "$fileName remote download successful, file size: ${targetFile.length()} bytes")
listener?.onLog("$fileName remote download successful")
DownloadResult(true, isRemoteSource = true)
} catch (e: SocketTimeoutException) {
Log.w(TAG, "$fileName download timeout", e)
DownloadResult(false, isRemoteSource = false, errorMessage = "Connection timeout")
} catch (e: IOException) {
Log.w(TAG, "$fileName network IO exception", e)
DownloadResult(false,
isRemoteSource = false,
errorMessage = "Network connection exception: ${e.message}"
)
} catch (e: Exception) {
Log.e(TAG, "$fileName exception occurred during download", e)
DownloadResult(false,
isRemoteSource = false,
errorMessage = "Download exception: ${e.message}"
)
} finally {
connection?.disconnect()
}
}
private suspend fun useLocalVersion(
fileName: String,
targetFile: File,
listener: DownloadProgressListener?
): DownloadResult = withContext(Dispatchers.IO) {
try {
com.sukisu.ultra.utils.AssetsUtil.exportFiles(context, fileName, targetFile.absolutePath)
if (!targetFile.exists()) {
val errorMsg = "Local $fileName file extraction failed"
listener?.onError(fileName, errorMsg)
return@withContext DownloadResult(false,
isRemoteSource = false,
errorMessage = errorMsg
)
}
if (!validateDownloadedFile(targetFile, fileName)) {
val errorMsg = "Local $fileName file verification failed"
listener?.onError(fileName, errorMsg)
return@withContext DownloadResult(
success = false,
isRemoteSource = false,
errorMessage = errorMsg
)
}
Log.i(TAG, "$fileName local version loaded successfully, file size: ${targetFile.length()} bytes")
listener?.onLog("$fileName local version loaded successfully")
listener?.onSuccess(fileName, false)
DownloadResult(true, isRemoteSource = false)
} catch (e: Exception) {
Log.e(TAG, "$fileName local version loading failed", e)
val errorMsg = "Local version loading failed: ${e.message}"
listener?.onError(fileName, errorMsg)
DownloadResult(success = false, isRemoteSource = false, errorMessage = errorMsg)
}
}
private fun validateDownloadedFile(file: File, fileName: String): Boolean {
if (!file.exists()) {
Log.w(TAG, "$fileName file does not exist")
return false
}
val fileSize = file.length()
if (fileSize < MIN_FILE_SIZE) {
Log.w(TAG, "$fileName file is too small: $fileSize bytes")
return false
}
try {
file.inputStream().use { input ->
val header = ByteArray(4)
val bytesRead = input.read(header)
if (bytesRead < 4) {
Log.w(TAG, "$fileName file header read incomplete")
return false
}
val isELF = header[0] == 0x7F.toByte() &&
header[1] == 'E'.code.toByte() &&
header[2] == 'L'.code.toByte() &&
header[3] == 'F'.code.toByte()
if (fileName == "kptools" && !isELF) {
Log.w(TAG, "kptools file format is invalid, not ELF format")
return false
}
Log.d(TAG, "$fileName file verification passed, size: $fileSize bytes, ELF: $isELF")
return true
}
} catch (e: Exception) {
Log.w(TAG, "$fileName file verification exception", e)
return false
}
}
private fun setExecutablePermission(filePath: String) {
try {
val process = Runtime.getRuntime().exec(arrayOf("su", "-c", "chmod a+rx $filePath"))
process.waitFor()
Log.d(TAG, "Set execution permission for $filePath")
} catch (e: Exception) {
Log.w(TAG, "Failed to set execution permission: $filePath", e)
try {
File(filePath).setExecutable(true, false)
} catch (ex: Exception) {
Log.w(TAG, "Java method to set permissions also failed", ex)
}
}
}
fun cleanup() {
try {
File(workDir).listFiles()?.forEach { file ->
if (file.name.endsWith(".tmp")) {
file.delete()
Log.d(TAG, "Cleaned temporary file: ${file.name}")
}
}
} catch (e: Exception) {
Log.w(TAG, "Failed to clean temporary files", e)
}
}
}

View File

@@ -19,6 +19,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.AutoFixHigh
import androidx.compose.material.icons.filled.FileUpload
import androidx.compose.material.icons.filled.Security
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
@@ -66,6 +67,8 @@ import com.sukisu.ultra.ui.util.*
fun InstallScreen(navigator: DestinationsNavigator) {
var installMethod by remember { mutableStateOf<InstallMethod?>(null) }
var lkmSelection by remember { mutableStateOf<LkmSelection>(LkmSelection.KmiNone) }
var kpmPatchEnabled by remember { mutableStateOf(false) }
var kpmUndoPatch by remember { mutableStateOf(false) }
val context = LocalContext.current
var showRebootDialog by remember { mutableStateOf(false) }
var showSlotSelectionDialog by remember { mutableStateOf(false) }
@@ -102,7 +105,9 @@ fun InstallScreen(navigator: DestinationsNavigator) {
navigator.navigate(
KernelFlashScreenDestination(
kernelUri = uri,
selectedSlot = method.slot
selectedSlot = method.slot,
kpmPatchEnabled = kpmPatchEnabled,
kpmUndoPatch = kpmUndoPatch
)
)
}
@@ -154,7 +159,6 @@ fun InstallScreen(navigator: DestinationsNavigator) {
}
}
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
Scaffold(
@@ -188,7 +192,12 @@ fun InstallScreen(navigator: DestinationsNavigator) {
} else {
installMethod = method
}
}
},
kpmPatchEnabled = kpmPatchEnabled,
onKpmPatchChanged = { kpmPatchEnabled = it },
kpmUndoPatch = kpmUndoPatch,
onKpmUndoPatchChanged = { kpmUndoPatch = it },
selectedMethod = installMethod
)
Column(
@@ -247,6 +256,33 @@ fun InstallScreen(navigator: DestinationsNavigator) {
)
}
}
// KPM 状态显示卡片
if (kpmPatchEnabled || kpmUndoPatch) {
ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
elevation = getCardElevation(),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp)
.clip(MaterialTheme.shapes.medium)
.shadow(
elevation = cardElevation,
shape = MaterialTheme.shapes.medium,
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
)
) {
Text(
text = stringResource(
if (kpmUndoPatch) R.string.kpm_undo_patch_enabled
else R.string.kpm_patch_enabled
),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(16.dp),
color = MaterialTheme.colorScheme.primary
)
}
}
}
Button(
@@ -327,7 +363,12 @@ sealed class InstallMethod {
@Composable
private fun SelectInstallMethod(
isGKI: Boolean = false,
onSelected: (InstallMethod) -> Unit = {}
onSelected: (InstallMethod) -> Unit = {},
kpmPatchEnabled: Boolean = false,
onKpmPatchChanged: (Boolean) -> Unit = {},
kpmUndoPatch: Boolean = false,
onKpmUndoPatchChanged: (Boolean) -> Unit = {},
selectedMethod: InstallMethod? = null
) {
val rootAvailable = rootAvailable()
val isAbDevice = isAbDevice()
@@ -335,9 +376,9 @@ private fun SelectInstallMethod(
val selectFileTip = stringResource(
id = R.string.select_file_tip,
if (isInitBoot()) {
"init_boot / vendor_boot ${stringResource(R.string.select_file_tip_vendor)}"
} else {
"boot"
"init_boot / vendor_boot ${stringResource(R.string.select_file_tip_vendor)}"
} else {
"boot"
}
)
@@ -537,7 +578,7 @@ private fun SelectInstallMethod(
elevation = getCardElevation(),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp)
.padding(bottom = if (selectedMethod is InstallMethod.HorizonKernel) 0.dp else 12.dp)
.clip(MaterialTheme.shapes.large)
) {
MaterialTheme(
@@ -635,6 +676,137 @@ private fun SelectInstallMethod(
}
}
}
// KPM 修补选项卡片
if (selectedMethod is InstallMethod.HorizonKernel && selectedMethod.uri != null) {
ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
elevation = getCardElevation(),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp)
.clip(MaterialTheme.shapes.large)
) {
MaterialTheme(
colorScheme = MaterialTheme.colorScheme.copy(
surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else MaterialTheme.colorScheme.surfaceVariant
)
) {
ListItem(
leadingContent = {
Icon(
Icons.Filled.Security,
contentDescription = null,
tint = MaterialTheme.colorScheme.tertiary
)
},
headlineContent = {
Text(
stringResource(R.string.kpm_patch_options),
style = MaterialTheme.typography.titleMedium
)
},
supportingContent = {
Text(
stringResource(R.string.kpm_patch_description),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
)
}
Column(
modifier = Modifier.padding(
start = 16.dp,
end = 16.dp,
bottom = 16.dp
)
) {
// KPM 修补开关
Row(
modifier = Modifier
.fillMaxWidth()
.clip(MaterialTheme.shapes.medium)
.clickable {
if (!kpmPatchEnabled) {
onKpmPatchChanged(true)
if (kpmUndoPatch) onKpmUndoPatchChanged(false)
} else {
onKpmPatchChanged(false)
}
}
.padding(vertical = 12.dp, horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Switch(
checked = kpmPatchEnabled,
onCheckedChange = { enabled ->
onKpmPatchChanged(enabled)
if (enabled && kpmUndoPatch) onKpmUndoPatchChanged(false)
},
colors = SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colorScheme.primary,
checkedTrackColor = MaterialTheme.colorScheme.primaryContainer
)
)
Spacer(modifier = Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = stringResource(R.string.enable_kpm_patch),
style = MaterialTheme.typography.bodyLarge
)
Text(
text = stringResource(R.string.kpm_patch_switch_description),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
// KPM 撤销修补开关
Row(
modifier = Modifier
.fillMaxWidth()
.clip(MaterialTheme.shapes.medium)
.clickable {
if (!kpmUndoPatch) {
onKpmUndoPatchChanged(true)
if (kpmPatchEnabled) onKpmPatchChanged(false)
} else {
onKpmUndoPatchChanged(false)
}
}
.padding(vertical = 12.dp, horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Switch(
checked = kpmUndoPatch,
onCheckedChange = { enabled ->
onKpmUndoPatchChanged(enabled)
if (enabled && kpmPatchEnabled) onKpmPatchChanged(false)
},
colors = SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colorScheme.tertiary,
checkedTrackColor = MaterialTheme.colorScheme.tertiaryContainer
)
)
Spacer(modifier = Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = stringResource(R.string.enable_kpm_undo_patch),
style = MaterialTheme.typography.bodyLarge
)
Text(
text = stringResource(R.string.kpm_undo_patch_switch_description),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
}
}
}
}
@@ -719,4 +891,4 @@ private fun TopBar(
@Composable
fun SelectInstallPreview() {
InstallScreen(EmptyDestinationsNavigator)
}
}

View File

@@ -6,9 +6,11 @@ import android.content.Context
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import com.sukisu.ultra.R
import com.sukisu.ultra.network.RemoteToolsDownloader
import com.sukisu.ultra.ui.util.rootAvailable
import com.sukisu.ultra.utils.AssetsUtil
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -16,6 +18,8 @@ import kotlinx.coroutines.flow.update
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
/**
@@ -78,14 +82,18 @@ class HorizonKernelState {
class HorizonKernelWorker(
private val context: Context,
private val state: HorizonKernelState,
private val slot: String? = null
private val slot: String? = null,
private val kpmPatchEnabled: Boolean = false,
private val kpmUndoPatch: Boolean = false
) : Thread() {
var uri: Uri? = null
private lateinit var filePath: String
private lateinit var binaryPath: String
private lateinit var workDir: String
private var onFlashComplete: (() -> Unit)? = null
private var originalSlot: String? = null
private var downloaderJob: Job? = null
fun setOnFlashCompleteListener(listener: () -> Unit) {
onFlashComplete = listener
@@ -97,6 +105,7 @@ class HorizonKernelWorker(
filePath = "${context.filesDir.absolutePath}/${DocumentFile.fromSingleUri(context, uri!!)?.name}"
binaryPath = "${context.filesDir.absolutePath}/META-INF/com/google/android/update-binary"
workDir = "${context.filesDir.absolutePath}/work"
try {
state.updateStep(context.getString(R.string.horizon_cleaning_files))
@@ -121,6 +130,20 @@ class HorizonKernelWorker(
state.updateProgress(0.4f)
getBinary()
// KPM修补
if (kpmPatchEnabled || kpmUndoPatch) {
state.updateStep(context.getString(R.string.kpm_preparing_tools))
state.updateProgress(0.5f)
prepareKpmToolsWithDownload()
state.updateStep(
if (kpmUndoPatch) context.getString(R.string.kpm_undoing_patch)
else context.getString(R.string.kpm_applying_patch)
)
state.updateProgress(0.55f)
performKpmPatch()
}
state.updateStep(context.getString(R.string.horizon_patching_script))
state.updateProgress(0.6f)
patch()
@@ -162,6 +185,206 @@ class HorizonKernelWorker(
state.updateProgress(0.8f)
runCommand(true, "resetprop ro.boot.slot_suffix $originalSlot")
}
} finally {
// 取消下载任务并清理
downloaderJob?.cancel()
cleanupDownloader()
}
}
private fun prepareKpmToolsWithDownload() {
try {
File(workDir).mkdirs()
val downloader = RemoteToolsDownloader(context, workDir)
val progressListener = object : RemoteToolsDownloader.DownloadProgressListener {
override fun onProgress(fileName: String, progress: Int, total: Int) {
val percentage = if (total > 0) (progress * 100) / total else 0
state.addLog("Downloading $fileName: $percentage% ($progress/$total bytes)")
}
override fun onLog(message: String) {
state.addLog(message)
}
override fun onError(fileName: String, error: String) {
state.addLog("Warning: $fileName - $error")
}
override fun onSuccess(fileName: String, isRemote: Boolean) {
val source = if (isRemote) "remote" else "local"
state.addLog("$fileName $source version prepared successfully")
}
}
val downloadJob = CoroutineScope(Dispatchers.IO).launch {
downloader.downloadToolsAsync(progressListener)
}
downloaderJob = downloadJob
runBlocking {
downloadJob.join()
}
val kptoolsPath = "$workDir/kptools"
val kpimgPath = "$workDir/kpimg"
if (!File(kptoolsPath).exists()) {
throw IOException("kptools file preparation failed")
}
if (!File(kpimgPath).exists()) {
throw IOException("kpimg file preparation failed")
}
runCommand(true, "chmod a+rx $kptoolsPath")
state.addLog("KPM tools preparation completed, starting patch operation")
} catch (_: CancellationException) {
state.addLog("KPM tools download cancelled")
throw IOException("Tool preparation process interrupted")
} catch (e: Exception) {
state.addLog("KPM tools preparation failed: ${e.message}")
state.addLog("Attempting to use legacy local file extraction...")
try {
prepareKpmToolsLegacy()
state.addLog("Successfully used local backup files")
} catch (legacyException: Exception) {
state.addLog("Local file extraction also failed: ${legacyException.message}")
throw IOException("Unable to prepare KPM tool files: ${e.message}")
}
}
}
private fun prepareKpmToolsLegacy() {
File(workDir).mkdirs()
val kptoolsPath = "$workDir/kptools"
val kpimgPath = "$workDir/kpimg"
AssetsUtil.exportFiles(context, "kptools", kptoolsPath)
if (!File(kptoolsPath).exists()) {
throw IOException("Local kptools file extraction failed")
}
AssetsUtil.exportFiles(context, "kpimg", kpimgPath)
if (!File(kpimgPath).exists()) {
throw IOException("Local kpimg file extraction failed")
}
runCommand(true, "chmod a+rx $kptoolsPath")
}
private fun cleanupDownloader() {
try {
val downloader = RemoteToolsDownloader(context, workDir)
downloader.cleanup()
} catch (_: Exception) {
}
}
/**
* 执行KMP修补操作
*/
private fun performKpmPatch() {
try {
// 创建临时解压目录
val extractDir = "$workDir/extracted"
File(extractDir).mkdirs()
// 解压压缩包到临时目录
val unzipResult = runCommand(true, "cd $extractDir && unzip -o \"$filePath\"")
if (unzipResult != 0) {
throw IOException(context.getString(R.string.kpm_extract_zip_failed))
}
// 查找Image文件
val findImageResult = runCommandGetOutput("find $extractDir -name 'Image' -type f")
if (findImageResult.isBlank()) {
throw IOException(context.getString(R.string.kpm_image_file_not_found))
}
val imageFile = findImageResult.lines().first().trim()
val imageDir = File(imageFile).parent
state.addLog(context.getString(R.string.kpm_found_image_file, imageFile))
// 复制KMP工具到Image文件所在目录
runCommand(true, "cp $workDir/kptools $imageDir/")
runCommand(true, "cp $workDir/kpimg $imageDir/")
// 执行KMP修补命令
val patchCommand = if (kpmUndoPatch) {
"cd $imageDir && chmod a+rx kptools && ./kptools -u -s 123 -i Image -k kpimg -o oImage && mv oImage Image"
} else {
"cd $imageDir && chmod a+rx kptools && ./kptools -p -s 123 -i Image -k kpimg -o oImage && mv oImage Image"
}
val patchResult = runCommand(true, patchCommand)
if (patchResult != 0) {
throw IOException(
if (kpmUndoPatch) context.getString(R.string.kpm_undo_patch_failed)
else context.getString(R.string.kpm_patch_failed)
)
}
state.addLog(
if (kpmUndoPatch) context.getString(R.string.kpm_undo_patch_success)
else context.getString(R.string.kpm_patch_success)
)
// 清理KMP工具文件
runCommand(true, "rm -f $imageDir/kptools $imageDir/kpimg $imageDir/oImage")
// 使用Java方式重新打包ZIP文件
val originalFileName = File(filePath).name
val patchedFilePath = "$workDir/patched_$originalFileName"
repackZipFolder(extractDir, patchedFilePath)
// 替换原始文件
runCommand(true, "mv \"$patchedFilePath\" \"$filePath\"")
state.addLog(context.getString(R.string.kpm_file_repacked))
} catch (e: Exception) {
state.addLog(context.getString(R.string.kpm_patch_operation_failed, e.message))
throw e
} finally {
// 清理临时文件
runCommand(true, "rm -rf $workDir")
}
}
private fun repackZipFolder(sourceDir: String, zipFilePath: String) {
try {
val buffer = ByteArray(1024)
val sourceFolder = File(sourceDir)
FileOutputStream(zipFilePath).use { fos ->
ZipOutputStream(fos).use { zos ->
sourceFolder.walkTopDown().forEach { file ->
if (file.isFile) {
val relativePath = file.relativeTo(sourceFolder).path
val zipEntry = ZipEntry(relativePath)
zos.putNextEntry(zipEntry)
file.inputStream().use { fis ->
var length: Int
while (fis.read(buffer).also { length = it } > 0) {
zos.write(buffer, 0, length)
}
}
zos.closeEntry()
}
}
}
}
} catch (e: Exception) {
throw IOException("Failed to create zip file: ${e.message}", e)
}
}
@@ -176,6 +399,7 @@ class HorizonKernelWorker(
private fun cleanup() {
runCommand(false, "find ${context.filesDir.absolutePath} -type f ! -name '*.jpg' ! -name '*.png' -delete")
runCommand(false, "rm -rf $workDir")
}
private fun copy() {
@@ -195,6 +419,7 @@ class HorizonKernelWorker(
}
}
@SuppressLint("StringFormatInvalid")
private fun patch() {
val kernelVersion = runCommandGetOutput("cat /proc/version")
val versionRegex = """\d+\.\d+\.\d+""".toRegex()
@@ -288,4 +513,4 @@ class HorizonKernelWorker(
private fun runCommandGetOutput(cmd: String): String {
return Shell.cmd(cmd).exec().out.joinToString("\n").trim()
}
}
}

View File

@@ -54,6 +54,8 @@ private object KernelFlashStateHolder {
var currentState: HorizonKernelState? = null
var currentUri: Uri? = null
var currentSlot: String? = null
var currentKpmPatchEnabled: Boolean = false
var currentKpmUndoPatch: Boolean = false
var isFlashing = false
}
@@ -66,7 +68,9 @@ private object KernelFlashStateHolder {
fun KernelFlashScreen(
navigator: DestinationsNavigator,
kernelUri: Uri,
selectedSlot: String? = null
selectedSlot: String? = null,
kpmPatchEnabled: Boolean = false,
kpmUndoPatch: Boolean = false
) {
val context = LocalContext.current
val scrollState = rememberScrollState()
@@ -79,13 +83,17 @@ fun KernelFlashScreen(
val horizonKernelState = remember {
if (KernelFlashStateHolder.currentState != null &&
KernelFlashStateHolder.currentUri == kernelUri &&
KernelFlashStateHolder.currentSlot == selectedSlot) {
KernelFlashStateHolder.currentSlot == selectedSlot &&
KernelFlashStateHolder.currentKpmPatchEnabled == kpmPatchEnabled &&
KernelFlashStateHolder.currentKpmUndoPatch == kpmUndoPatch) {
KernelFlashStateHolder.currentState!!
} else {
HorizonKernelState().also {
KernelFlashStateHolder.currentState = it
KernelFlashStateHolder.currentUri = kernelUri
KernelFlashStateHolder.currentSlot = selectedSlot
KernelFlashStateHolder.currentKpmPatchEnabled = kpmPatchEnabled
KernelFlashStateHolder.currentKpmUndoPatch = kpmUndoPatch
KernelFlashStateHolder.isFlashing = false
}
}
@@ -107,7 +115,9 @@ fun KernelFlashScreen(
val worker = HorizonKernelWorker(
context = context,
state = horizonKernelState,
slot = selectedSlot
slot = selectedSlot,
kpmPatchEnabled = kpmPatchEnabled,
kpmUndoPatch = kpmUndoPatch
)
worker.uri = kernelUri
worker.setOnFlashCompleteListener(onFlashComplete)
@@ -147,6 +157,8 @@ fun KernelFlashScreen(
KernelFlashStateHolder.currentState = null
KernelFlashStateHolder.currentUri = null
KernelFlashStateHolder.currentSlot = null
KernelFlashStateHolder.currentKpmPatchEnabled = false
KernelFlashStateHolder.currentKpmUndoPatch = false
KernelFlashStateHolder.isFlashing = false
}
navigator.popBackStack()
@@ -216,7 +228,7 @@ fun KernelFlashScreen(
.padding(innerPadding)
.nestedScroll(scrollBehavior.nestedScrollConnection),
) {
FlashProgressIndicator(flashState)
FlashProgressIndicator(flashState, kpmPatchEnabled, kpmUndoPatch)
Box(
modifier = Modifier
.fillMaxWidth()
@@ -239,7 +251,11 @@ fun KernelFlashScreen(
}
@Composable
private fun FlashProgressIndicator(flashState: FlashState) {
private fun FlashProgressIndicator(
flashState: FlashState,
kpmPatchEnabled: Boolean = false,
kpmUndoPatch: Boolean = false
) {
val progressColor = when {
flashState.error.isNotEmpty() -> MaterialTheme.colorScheme.error
flashState.isCompleted -> MaterialTheme.colorScheme.tertiary
@@ -298,6 +314,17 @@ private fun FlashProgressIndicator(flashState: FlashState) {
}
}
// KPM状态显示
if (kpmPatchEnabled || kpmUndoPatch) {
Spacer(modifier = Modifier.height(4.dp))
Text(
text = if (kpmUndoPatch) stringResource(R.string.kpm_undo_patch_mode)
else stringResource(R.string.kpm_patch_mode),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.tertiary
)
}
Spacer(modifier = Modifier.height(8.dp))
if (flashState.currentStep.isNotEmpty()) {

View File

@@ -620,4 +620,34 @@
<string name="module_signature_invalid_message">未经签名的模块可能不完整。为了对设备进行保护,已阻止安装此模块。</string>
<string name="module_signature_verification_failed">未经签名的模块可能不完整。你想安装来自未知发布者的模块吗?</string>
<string name="home_hook_type">钩子类型</string>
<!-- KPM修补相关字符串 -->
<string name="kpm_patch_options">KPM修补</string>
<string name="kpm_patch_description">用于添加附加的KPM功能</string>
<string name="enable_kpm_patch">KPM修补</string>
<string name="kpm_patch_switch_description">在刷写前对内核镜像进行KPM修补</string>
<string name="enable_kpm_undo_patch">KPM撤销修补</string>
<string name="kpm_undo_patch_switch_description">撤销之前应用的KPM修补</string>
<string name="kpm_patch_enabled">KPM修补已启用</string>
<string name="kpm_undo_patch_enabled">KPM撤销修补已启用</string>
<string name="kpm_patch_mode">KPM修补模式</string>
<string name="kpm_undo_patch_mode">KPM撤销修补模式</string>
<!-- KPM工作流程相关 -->
<string name="kpm_preparing_tools">准备KPM修补工具</string>
<string name="kpm_applying_patch">正在应用KPM修补</string>
<string name="kpm_undoing_patch">正在撤销KPM修补</string>
<string name="kpm_tools_prepared">KPM工具准备完成</string>
<string name="kpm_found_image_file">找到Image文件: %s</string>
<string name="kpm_patch_success">KPM修补成功</string>
<string name="kpm_undo_patch_success">KPM撤销修补成功</string>
<string name="kpm_file_repacked">文件重新打包完成</string>
<!-- KPM错误信息 -->
<string name="kpm_extract_kptools_failed">提取kptools工具失败</string>
<string name="kpm_extract_kpimg_failed">提取kpimg文件失败</string>
<string name="kpm_prepare_tools_failed">准备KPM工具失败: %s</string>
<string name="kpm_extract_zip_failed">解压压缩包失败</string>
<string name="kpm_image_file_not_found">未找到Image文件</string>
<string name="kpm_patch_failed">KPM修补失败</string>
<string name="kpm_undo_patch_failed">KPM撤销修补失败</string>
<string name="kpm_repack_zip_failed">重新打包压缩文件失败</string>
<string name="kpm_patch_operation_failed">KPM修补操作失败: %s</string>
</resources>

View File

@@ -628,4 +628,34 @@ Important Note:\n
<string name="module_signature_invalid_message">Unsigned modules may be incomplete. To protect your device, installation of this module has been blocked</string>
<string name="module_signature_verification_failed">Unsigned modules may be incomplete. Do you want to allow the following module from an unknown publisher to install in this device?</string>
<string name="home_hook_type">Hook type</string>
<!-- KPM patching related strings -->
<string name="kpm_patch_options">KPM Patch</string>
<string name="kpm_patch_description">For adding additional KPM features</string>
<string name="enable_kpm_patch">KPM Patch</string>
<string name="kpm_patch_switch_description">Apply KPM patch to kernel image before flashing</string>
<string name="enable_kpm_undo_patch">KPM Undo Patch</string>
<string name="kpm_undo_patch_switch_description">Undo previously applied KPM patch</string>
<string name="kpm_patch_enabled">KPM patch enabled</string>
<string name="kpm_undo_patch_enabled">KPM undo patch enabled</string>
<string name="kpm_patch_mode">KPM Patch Mode</string>
<string name="kpm_undo_patch_mode">KPM Undo Patch Mode</string>
<!-- KPM workflow related -->
<string name="kpm_preparing_tools">Preparing KPM tools</string>
<string name="kpm_applying_patch">Applying KPM patch</string>
<string name="kpm_undoing_patch">Undoing KPM patch</string>
<string name="kpm_tools_prepared">KPM tools prepared</string>
<string name="kpm_found_image_file">Found Image file: %s</string>
<string name="kpm_patch_success">KPM patch applied successfully</string>
<string name="kpm_undo_patch_success">KPM patch undone successfully</string>
<string name="kpm_file_repacked">File repacked successfully</string>
<!-- KPM error messages -->
<string name="kpm_extract_kptools_failed">Failed to extract kptools</string>
<string name="kpm_extract_kpimg_failed">Failed to extract kpimg file</string>
<string name="kpm_prepare_tools_failed">Failed to prepare KPM tools: %s</string>
<string name="kpm_extract_zip_failed">Failed to extract zip file</string>
<string name="kpm_image_file_not_found">Image file not found</string>
<string name="kpm_patch_failed">KPM patch failed</string>
<string name="kpm_undo_patch_failed">KPM undo patch failed</string>
<string name="kpm_repack_zip_failed">Failed to repack zip file</string>
<string name="kpm_patch_operation_failed">KPM patch operation failed: %s</string>
</resources>