Skip to content

Commit

Permalink
feat: 支持同时签名多个文件/查看多个文件签名
Browse files Browse the repository at this point in the history
  • Loading branch information
jixiaoyong committed Jan 26, 2024
1 parent 240b187 commit b07981f
Show file tree
Hide file tree
Showing 5 changed files with 176 additions and 50 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ APK 文件。
- [x] 查看 APK 已有签名信息
- [x] 支持 Light 和 Dark 主题,并可自动切换
- [x] 可选对齐与否
- [ ] 支持多文件签名
- [x] 支持多文件签名,查看签名
- [ ] 优化签名配置
- [ ] 美化主题

Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ fun App(window: ComposeWindow) {
}
}

var currentApkFilePath by remember { mutableStateOf<String?>(null) }
var currentApkFilePath by remember { mutableStateOf<List<String>>(emptyList()) }
var isDarkTheme by remember { mutableStateOf(false) }
val newSignInfo = remember { mutableStateOf(SignInfoBean()) }

Expand Down
152 changes: 111 additions & 41 deletions src/main/kotlin/io/github/jixiaoyong/pages/signapp/SignApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,21 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.awt.ComposeWindow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Popup
import io.github.jixiaoyong.utils.FileChooseUtil
import io.github.jixiaoyong.utils.SettingsTool
import io.github.jixiaoyong.utils.StorageKeys
import io.github.jixiaoyong.widgets.ButtonWidget
import io.github.jixiaoyong.widgets.InfoItemWidget
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch
import java.awt.Desktop
Expand All @@ -51,8 +54,6 @@ import kotlin.math.roundToInt
* 2. 开始签名
* 3. 签名历史
*
* todo 添加可以自定义签名文件输出地址的功能
*
* @email : jixiaoyong1995@gmail.com
* @date : 2023/8/18
*/
Expand All @@ -61,39 +62,58 @@ import kotlin.math.roundToInt
@Composable
fun PageSignApp(
window: ComposeWindow,
currentApkFilePath: String?,
currentApkFilePath: List<String>,
settings: SettingsTool,
onChangeApk: (String) -> Unit,
onChangeApk: (List<String>) -> Unit,
onChangePage: (String) -> Unit
) {

val scope = rememberCoroutineScope()
var signLogs by remember { mutableStateOf(listOf<String>()) }
var signApkResult: CommandResult by remember { mutableStateOf(CommandResult.NOT_EXECUT) }
val scaffoldState = rememberScaffoldState()
val isEnabled = remember(signApkResult) {
CommandResult.NOT_EXECUT == signApkResult
}

val apkSignType by settings.signTypeList.collectAsState(setOf())
val selectedSignInfo by settings.selectedSignInfoBean.collectAsState(null)
val signedDirectory by settings.signedDirectory.collectAsState(null)
val isZipAlign by settings.isZipAlign.collectAsState(false)

var signInfoResult: CommandResult by remember { mutableStateOf(CommandResult.NOT_EXECUT) }


val local = signInfoResult
when (local) {
is CommandResult.Success<*> -> {
AlertDialog(onDismissRequest = {
Popup(onDismissRequest = {
signInfoResult = CommandResult.NOT_EXECUT
}, buttons = {

}, title = { Text("查询签名结果") }, text = {
SelectionContainer {
Text(local.result?.toString() ?: "")
}, alignment = Alignment.Center) {
Column(
modifier = Modifier.fillMaxSize().background(color = MaterialTheme.colors.onBackground.copy(0.2f))
.padding(horizontal = 50.dp, vertical = 65.dp)
.background(MaterialTheme.colors.surface, shape = RoundedCornerShape(10.dp))
.padding(horizontal = 20.dp, vertical = 15.dp)
) {
Text(
"签名信息(鼠标上下滚动查看更多)",
color = MaterialTheme.colors.onSurface,
fontWeight = FontWeight.W800,
modifier = Modifier.padding(20.dp).align(alignment = Alignment.CenterHorizontally)
)
Column(
modifier = Modifier.weight(1f, fill = false).heightIn(max = 450.dp)
.verticalScroll(rememberScrollState())
) {
SelectionContainer {
Text(
local.result?.toString() ?: "",
color = MaterialTheme.colors.onSurface
)
}
}
TextButton(onClick = {
signInfoResult = CommandResult.NOT_EXECUT
}, modifier = Modifier.align(alignment = Alignment.CenterHorizontally)) { Text("确认") }
}
})
}
}

is CommandResult.Error<*> -> {
Expand All @@ -105,6 +125,25 @@ fun PageSignApp(
else -> {}
}

if (local is CommandResult.EXECUTING || signApkResult is CommandResult.EXECUTING) {
Popup(alignment = Alignment.Center) {
Box(
modifier = Modifier.fillMaxSize().background(color = MaterialTheme.colors.onBackground.copy(0.2f)),
contentAlignment = Alignment.Center
) {
Column(
modifier = Modifier.size(150.dp)
.background(color = Color.Black.copy(0.8f), shape = RoundedCornerShape(5.dp))
.padding(20.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
CircularProgressIndicator(modifier = Modifier.size(80.dp).padding(10.dp))
Text("处理中……", color = Color.White.copy(0.8f))
}
}
}
}

Scaffold(scaffoldState = scaffoldState) {

Column(horizontalAlignment = Alignment.CenterHorizontally) {
Expand All @@ -125,15 +164,20 @@ fun PageSignApp(
.clickable {
scope.launch {
val chooseFileName =
FileChooseUtil.chooseSignFile(window, "请选择要签名的apk文件")
if (chooseFileName.isNullOrBlank()) {
FileChooseUtil.chooseMultiFile(
window,
"请选择要签名的apk文件",
filter = { _, name ->
name.toLowerCase(Locale.getDefault()).endsWith(".apk")
})
if (chooseFileName.isNullOrEmpty()) {
scaffoldState.snackbarHostState.showSnackbar("请选择要签名的apk文件")
} else {
onChangeApk(chooseFileName)
if (!signedDirectory.isNullOrBlank()) {
settings.save(
StorageKeys.SIGNED_DIRECTORY,
chooseFileName.substringBeforeLast(File.separator)
chooseFileName.first().substringBeforeLast(File.separator)
)
}
scaffoldState.snackbarHostState.showSnackbar("修改成功")
Expand All @@ -143,11 +187,10 @@ fun PageSignApp(
component = JPanel(),
onFileDrop = {
scope.launch {
val file =
it.firstOrNull {
it.lowercase(Locale.getDefault()).endsWith(".apk")
}
if (null == file) {
val file = it.filter() {
it.lowercase(Locale.getDefault()).endsWith(".apk")
}
if (file.isNullOrEmpty()) {
scaffoldState.snackbarHostState.showSnackbar("请先选择正确的apk文件")
} else {
onChangeApk(file)
Expand All @@ -157,21 +200,23 @@ fun PageSignApp(
}
) {
Text(
text = "请拖拽apk文件到这里哦\n(也可以点击这里选择apk文件)",
text = "请拖拽apk文件到这里哦\n(支持多选,也可以点击这里选择apk文件)",
textAlign = TextAlign.Center,
modifier = Modifier.align(alignment = Alignment.Center)
)
}
InfoItemWidget(
"当前选择的文件",
currentApkFilePath ?: "请先选择apk文件",
"当前选择的文件${if (currentApkFilePath.isEmpty()) "" else "(" + currentApkFilePath.size + ")"}",
if (currentApkFilePath.isEmpty()) "请先选择apk文件" else currentApkFilePath.joinToString("\n"),
buttonTitle = "查看签名",
onClick = {
scope.launch {
if (currentApkFilePath.isNullOrBlank()) {
scope.launch(Dispatchers.IO) {
if (currentApkFilePath.isEmpty()) {
scaffoldState.snackbarHostState.showSnackbar("请先选择apk文件")
} else {
signInfoResult = ApkSigner.getApkSignInfo(currentApkFilePath)
signInfoResult = CommandResult.EXECUTING
val resultList = currentApkFilePath.map { ApkSigner.getApkSignInfo(it) }
signInfoResult = mergeCommandResult(resultList, currentApkFilePath)
}
}
}
Expand All @@ -194,7 +239,7 @@ fun PageSignApp(
FileChooseUtil.chooseSignDirectory(
window,
errorTips,
signedDirectory ?: currentApkFilePath
signedDirectory ?: currentApkFilePath.firstOrNull()
)
if (outputDirectory.isNullOrBlank()) {
scaffoldState.snackbarHostState.showSnackbar(errorTips)
Expand Down Expand Up @@ -269,11 +314,9 @@ fun PageSignApp(
) {
ButtonWidget(
{
scope.launch {
if (currentApkFilePath.isNullOrBlank() || !currentApkFilePath.lowercase(
Locale.getDefault()
)
.endsWith(".apk")
scope.launch(Dispatchers.IO) {
if (currentApkFilePath.filter { it.lowercase(Locale.getDefault()).endsWith(".apk") }
.isNullOrEmpty()
) {
scaffoldState.snackbarHostState.showSnackbar("请先选择正确的apk文件")
return@launch
Expand All @@ -297,6 +340,7 @@ fun PageSignApp(
return@launch
}

signApkResult = CommandResult.EXECUTING
val signResult = ApkSigner.alignAndSignApk(
currentApkFilePath,
localSelectedSignInfo.keyStorePath,
Expand All @@ -320,30 +364,34 @@ fun PageSignApp(
}
)

signApkResult = signResult
if (signResult is CommandResult.Success<*> && !signResult.result?.toString()
val mergedResult = mergeCommandResult(signResult, currentApkFilePath)
signApkResult = mergedResult
val firstSuccessSignedApk =
signResult.firstOrNull { it is CommandResult.Success<*> } as CommandResult.Success<*>

if (mergedResult is CommandResult.Success<*> && !firstSuccessSignedApk.result?.toString()
.isNullOrBlank()
) {
val result = scaffoldState.snackbarHostState.showSnackbar(
"签名成功,是否打开签名后的文件?",
"打开",
SnackbarDuration.Long
)
val file = File(signResult.result?.toString() ?: "")
val file = File(firstSuccessSignedApk.result?.toString() ?: "")
if (SnackbarResult.ActionPerformed == result && file.exists()) {
Desktop.getDesktop().open(file.parentFile)
}
} else if (signResult is CommandResult.Error<*>) {
} else if (mergedResult is CommandResult.Error<*>) {
scaffoldState.snackbarHostState.showSnackbar(
"签名失败:${signResult.message}",
"签名失败:${mergedResult.message}",
)
}

signApkResult = CommandResult.NOT_EXECUT
}

},
enabled = isEnabled,
enabled = CommandResult.NOT_EXECUT == signApkResult,
title = "开始签名apk",
modifier = Modifier.size(250.dp, 50.dp),
)
Expand All @@ -352,6 +400,28 @@ fun PageSignApp(
}
}

private const val TITLE_CONTENT_DIVIDER = "-------------------------------------------------------"
private fun mergeCommandResult(resultList: List<CommandResult>, pathList: List<String>) =

if (resultList.filter { it is CommandResult.Success<*> }.isNotEmpty()) {
val message = List(resultList.size) { index ->
val path = pathList.getOrNull(index)
val result = resultList.getOrNull(index)
"$TITLE_CONTENT_DIVIDER\n[${index + 1}] $path\n$TITLE_CONTENT_DIVIDER\n" + (if (result is CommandResult.Error<*>) result.message.toString()
else (result as CommandResult.Success<*>).result.toString())
}.joinToString("\n\n")

CommandResult.Success(resultList.joinToString { message })
} else {
val message = List(resultList.size) { index ->
val path = pathList.getOrNull(index)
val result = resultList.getOrNull(index)
"$TITLE_CONTENT_DIVIDER\n[${index + 1}] $path\n$TITLE_CONTENT_DIVIDER\n" + ((result as CommandResult.Error<*>).message.toString())
}.joinToString("\n\n")

CommandResult.Error(message)
}

@Composable
fun DropBoxPanel(
window: ComposeWindow,
Expand Down
51 changes: 45 additions & 6 deletions src/main/kotlin/io/github/jixiaoyong/utils/ApkSigner.kt
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,43 @@ object ApkSigner {
}
}

/**
* 对齐并签名APK文件,签名后的文件添加_signed后缀,即x_signed.apk
* @param apkFilePathList 待签名的apk文件绝对路径的列表
* @param keyStorePath 签名文件路径
* @param keyAlias 表示 signer 在密钥库中的私钥和证书数据的别名的名称
* @param keyStorePwd 包含 signer 私钥和证书的密钥库的密码
* @param keyPwd signer 私钥的密码。
* @param zipAlign 是否需要对齐
* @param signedApkDirectory 签名之后的文件输出路径,默认为apkFilePath对应的x.apk所在的文件夹
* @return 返回结果 CommandResult 成功或失败,及信息
*/
fun alignAndSignApk(
apkFilePathList: List<String>,
keyStorePath: String,
keyAlias: String,
keyStorePwd: String,
keyPwd: String,
signedApkDirectory: String? = null,
zipAlign: Boolean = true,
signVersions: List<SignType> = SignType.DEF_SIGN_TYPES,
onProgress: (String) -> Unit
): List<CommandResult> {
return apkFilePathList.map {
alignAndSignApk(
it,
keyStorePath,
keyAlias,
keyStorePwd,
keyPwd,
signedApkDirectory,
zipAlign,
signVersions,
onProgress
)
}
}

/**
* 对齐并签名APK文件,签名后的文件添加_signed后缀,即x_signed.apk
* @param apkFilePath 待签名的apk文件绝对路径
Expand Down Expand Up @@ -195,13 +232,13 @@ object ApkSigner {
} catch (e: Exception) {
Logger.error("签名失败", e)
return CommandResult.Error("${e.message}", e)
}finally {
} finally {
if (zipAlign) {
try {
File(alignedApkFilePath).delete()
}catch (e: Exception) {
Logger.error("删除文件失败", e)
}
try {
File(alignedApkFilePath).delete()
} catch (e: Exception) {
Logger.error("删除文件失败", e)
}
}
}
}
Expand Down Expand Up @@ -305,4 +342,6 @@ sealed class CommandResult {
class Error<T>(val message: T, val error: Exception? = null) : CommandResult()

object NOT_EXECUT : CommandResult()

object EXECUTING : CommandResult()
}

0 comments on commit b07981f

Please sign in to comment.