新增打包签名配置和密钥库管理

- 打包功能新增签名配置功能,支持设置签名密钥和签名方案(V1、V1 + V2等)
- 新增密钥库管理,支持创建和删除自定义签名密钥库等
This commit is contained in:
luckyloogn
2024-12-31 19:52:24 +08:00
parent 09c1069d16
commit 925f22ccc8
37 changed files with 2276 additions and 1 deletions

View File

@@ -8,7 +8,10 @@ import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Build
import android.util.Log
import com.mcal.apksigner.ApkSigner
import com.reandroid.arsc.chunk.TableBlock
import org.apache.commons.io.FileUtils.copyFile
import org.apache.commons.io.FileUtils.copyInputStreamToFile
import org.autojs.autojs.app.GlobalAppContext
import org.autojs.autojs.engine.encryption.AdvancedEncryptionStandard
import org.autojs.autojs.pio.PFiles
@@ -16,6 +19,7 @@ import org.autojs.autojs.project.BuildInfo
import org.autojs.autojs.project.ProjectConfig
import org.autojs.autojs.script.EncryptedScriptFileHeader.writeHeader
import org.autojs.autojs.script.JavaScriptFileSource
import org.autojs.autojs.apkbuilder.keystore.KeyStore
import org.autojs.autojs.util.FileUtils.TYPE.JAVASCRIPT
import org.autojs.autojs.util.MD5Utils
import org.autojs.autojs6.BuildConfig
@@ -269,6 +273,40 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File
val fos = FileOutputStream(outApkFile)
TinySign.sign(File(workspacePath), fos)
fos.close()
val defaultKeyStoreFile = File(workspacePath, "default_key_store.bks")
val tmpOutputApk = File(workspacePath, "temp.apk")
copyInputStreamToFile(GlobalAppContext.get().assets.open("default_key_store.bks"), defaultKeyStoreFile)
val signer = ApkSigner(outApkFile, tmpOutputApk)
signer.useDefaultSignatureVersion = false
signer.v1SigningEnabled = mAppConfig.signatureSchemes.contains("V1")
signer.v2SigningEnabled = mAppConfig.signatureSchemes.contains("V2")
signer.v3SigningEnabled = mAppConfig.signatureSchemes.contains("V3")
signer.v4SigningEnabled = mAppConfig.signatureSchemes.contains("V4")
var keyStoreFile = defaultKeyStoreFile
var password = "AutoJs6"
var alias = "AutoJs6"
var aliasPassword = "AutoJs6"
mAppConfig.keyStore?.let {
keyStoreFile = File(it.absolutePath)
password = it.password
alias = it.alias
aliasPassword = it.aliasPassword
}
// 使用 ApkSigner 重新签名
if (!signer.signRelease(keyStoreFile, password, alias, aliasPassword)) {
throw java.lang.RuntimeException("Failed to re-sign using ApkSigner")
}
try {
copyFile(tmpOutputApk, outApkFile)
} catch (e: java.lang.Exception) {
throw java.lang.RuntimeException(e)
}
}
fun cleanWorkspace() = also {
@@ -321,6 +359,10 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File
private set
var libs: List<String> = emptyList()
private set
var keyStore: KeyStore? = null
private set
var signatureSchemes: String = "V1 + V2"
private set
fun ignoreDir(dir: File) = also { ignoredDirs.add(dir) }
@@ -342,6 +384,10 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File
fun setLibs(libs: List<String>) = also { this.libs = libs }
fun setKeyStore(keyStore: KeyStore?) = also { this.keyStore = keyStore }
fun setSignatureSchemes(signatureSchemes: String) = also { this.signatureSchemes = signatureSchemes }
companion object {
@JvmStatic
fun fromProjectConfig(projectDir: String?, projectConfig: ProjectConfig) = AppConfig()

View File

@@ -0,0 +1,18 @@
package org.autojs.autojs.apkbuilder.keystore
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class KeyStore(
@PrimaryKey val absolutePath: String, // 密钥库绝对路径
@ColumnInfo(name = "filename") val filename: String = "", // 文件名
@ColumnInfo(name = "password") val password: String = "", // 密码
@ColumnInfo(name = "alias") val alias: String = "", // 别名
@ColumnInfo(name = "alias_password") val aliasPassword: String = "", // 别名密码
@ColumnInfo(name = "verified") val verified: Boolean = false, // 验证状态
) {
override fun toString(): String = filename
}

View File

@@ -0,0 +1,29 @@
package org.autojs.autojs.apkbuilder.keystore
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Query
import androidx.room.Upsert
@Dao
interface KeyStoreDao {
@Query("SELECT * FROM keystore WHERE absolutePath = :absolutePath LIMIT 1")
suspend fun getByAbsolutePath(absolutePath: String): KeyStore?
@Upsert
suspend fun upsert(vararg keyStores: KeyStore)
@Query("SELECT * FROM keystore")
suspend fun getAll(): List<KeyStore>
@Delete
suspend fun delete(vararg keyStores: KeyStore)
@Query("DELETE FROM keystore WHERE absolutePath = :absolutePath")
suspend fun deleteByAbsolutePath(absolutePath: String): Int
@Query("DELETE FROM keystore")
suspend fun deleteAll()
}

View File

@@ -0,0 +1,28 @@
package org.autojs.autojs.apkbuilder.keystore
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
@Database(entities = [KeyStore::class], version = 1, exportSchema = false)
abstract class KeyStoreDatabase : RoomDatabase() {
abstract fun keyStoreDao(): KeyStoreDao
companion object {
@Volatile
private var INSTANCE: KeyStoreDatabase? = null
fun getDatabase(context: Context): KeyStoreDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
KeyStoreDatabase::class.java,
"keystore-database"
).build()
INSTANCE = instance
instance
}
}
}
}

View File

@@ -0,0 +1,50 @@
package org.autojs.autojs.apkbuilder.keystore
import android.content.Context
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class KeyStoreRepository(context: Context) {
private var dao: KeyStoreDao
init {
val keyStoreDatabase = KeyStoreDatabase.getDatabase(context)
dao = keyStoreDatabase.keyStoreDao()
}
// 获取所有 KeyStore
suspend fun getAllKeyStores(): List<KeyStore> {
return withContext(Dispatchers.IO) {
dao.getAll()
}
}
// 插入或更新 KeyStore
suspend fun upsertKeyStores(vararg keyStores: KeyStore) {
withContext(Dispatchers.IO) {
dao.upsert(*keyStores)
}
}
// 根据绝对路径获取 KeyStore
suspend fun getKeyStoreAbsolutePath(absolutePath: String): KeyStore? {
return withContext(Dispatchers.IO) {
dao.getByAbsolutePath(absolutePath)
}
}
// 删除 KeyStore
suspend fun deleteKeyStores(vararg keyStores: KeyStore) {
withContext(Dispatchers.IO) {
dao.delete(*keyStores)
}
}
// 删除所有 KeyStore
suspend fun deleteAllKeyStores() {
withContext(Dispatchers.IO) {
dao.deleteAll()
}
}
}

View File

@@ -178,7 +178,7 @@ public class ConsoleView extends FrameLayout implements ConsoleImpl.LogListener
return /* default text size */ 14;
}
public void setTextColors(@NotNull Integer[] colors) {
public void setTextColors(Integer[] colors) {
Adapter adapter = (Adapter) mLogListRecyclerView.getAdapter();
if (adapter != null) {
adapter.setTextColors(colors);

View File

@@ -172,6 +172,11 @@ object Pref {
@ScriptInterfaceCompatible
fun getScriptDirPath() = WorkingDirectoryUtils.path
@JvmStatic
fun getKeyStorePath(): String {
return getScriptDirPath() + "/.KeyStore/"
}
@JvmStatic
fun registerOnSharedPreferenceChangeListener(listener: OnSharedPreferenceChangeListener) {
sPref.registerOnSharedPreferenceChangeListener(listener)

View File

@@ -0,0 +1,82 @@
package org.autojs.autojs.ui.keystore
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import org.autojs.autojs.apkbuilder.keystore.KeyStore
import org.autojs.autojs6.R
import org.autojs.autojs6.databinding.ItemKeyStoreBinding
class KeyStoreAdaptor(
private val keyStoreAdapterCallback: KeyStoreAdapterCallback,
) : ListAdapter<KeyStore, KeyStoreAdaptor.KeyStoreViewHolder>(KeyStoreDiffCallback()) {
class KeyStoreDiffCallback : DiffUtil.ItemCallback<KeyStore>() {
override fun areItemsTheSame(oldItem: KeyStore, newItem: KeyStore): Boolean {
return oldItem.absolutePath == newItem.absolutePath
}
override fun areContentsTheSame(oldItem: KeyStore, newItem: KeyStore): Boolean {
return oldItem.filename == newItem.filename &&
oldItem.password == newItem.password &&
oldItem.alias == newItem.alias &&
oldItem.aliasPassword == newItem.aliasPassword &&
oldItem.verified == newItem.verified
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): KeyStoreViewHolder {
val binding = ItemKeyStoreBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return KeyStoreViewHolder(binding).apply {
binding.delete.setOnClickListener {
if (bindingAdapterPosition != RecyclerView.NO_POSITION) {
keyStoreAdapterCallback.onDeleteButtonClicked(getItem(bindingAdapterPosition))
}
}
binding.verify.setOnClickListener {
if (bindingAdapterPosition != RecyclerView.NO_POSITION) {
keyStoreAdapterCallback.onVerifyButtonClicked(getItem(bindingAdapterPosition))
}
}
itemView.setOnClickListener {
if (bindingAdapterPosition != RecyclerView.NO_POSITION) {
keyStoreAdapterCallback.onVerifyButtonClicked(getItem(bindingAdapterPosition))
}
}
}
}
override fun onBindViewHolder(holder: KeyStoreViewHolder, position: Int) {
holder.bind(getItem(position))
}
inner class KeyStoreViewHolder(private val binding: ItemKeyStoreBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: KeyStore) {
binding.apply {
filename.text = itemView.context.getString(
R.string.text_str_colon_space_str_formatter,
itemView.context.getString(R.string.text_file_name),
item.filename
)
alias.text = itemView.context.getString(
R.string.text_str_colon_space_str_formatter,
itemView.context.getString(R.string.text_key_alias),
item.alias
)
verify.setImageResource(
if (item.verified) R.drawable.ic_key_store_verified else R.drawable.ic_key_store_unverified
)
}
}
}
interface KeyStoreAdapterCallback {
fun onDeleteButtonClicked(keyStore: KeyStore)
fun onVerifyButtonClicked(keyStore: KeyStore)
}
}

View File

@@ -0,0 +1,266 @@
package org.autojs.autojs.ui.keystore
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import com.afollestad.materialdialogs.DialogAction
import com.afollestad.materialdialogs.MaterialDialog
import com.mcal.apksigner.CertCreator
import com.mcal.apksigner.utils.DistinguishedNameValues
import com.mcal.apksigner.utils.KeyStoreHelper
import org.autojs.autojs.apkbuilder.keystore.KeyStore
import org.autojs.autojs.core.pref.Pref
import org.autojs.autojs6.R
import org.autojs.autojs6.databinding.ActivityManageKeyStoreBinding
import org.autojs.autojs.ui.BaseActivity
import org.autojs.autojs.ui.keystore.NewKeyStoreDialog.NewKeyStoreConfigs
import org.autojs.autojs.ui.viewmodel.KeyStoreViewModel
import java.io.File
import java.io.IOException
class ManageKeyStoreActivity : BaseActivity() {
private lateinit var binding: ActivityManageKeyStoreBinding
private lateinit var keyStoreAdapter: KeyStoreAdaptor
private lateinit var keyStoreViewModel: KeyStoreViewModel
companion object {
fun startActivity(context: Context) {
Intent(context, ManageKeyStoreActivity::class.java).apply {}.also {
ContextCompat.startActivity(context, it, null)
}
}
}
private val newKeyStoreDialogCallback = object : NewKeyStoreDialog.Callback {
override fun onConfirmButtonClicked(configs: NewKeyStoreConfigs) {
createKeyStore(configs)
}
}
private val verifyKeyStoreDialog = object : VerifyKeyStoreDialog.Callback {
override fun onVerifyButtonClicked(
configs: VerifyKeyStoreDialog.VerifyKeyStoreConfigs, keyStore: KeyStore,
) {
verifyKeyStore(configs, keyStore)
}
}
private val keyStoreAdapterCallback = object : KeyStoreAdaptor.KeyStoreAdapterCallback {
override fun onDeleteButtonClicked(keyStore: KeyStore) {
MaterialDialog.Builder(this@ManageKeyStoreActivity)
.title(getString(R.string.text_confirm_to_delete))
.positiveText(R.string.text_ok).negativeText(R.string.text_cancel)
.onPositive { _: MaterialDialog, _: DialogAction ->
deleteKeyStore(keyStore)
}.show()
}
override fun onVerifyButtonClicked(keyStore: KeyStore) {
VerifyKeyStoreDialog(verifyKeyStoreDialog, keyStore).show(supportFragmentManager, null)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityManageKeyStoreBinding.inflate(layoutInflater)
setContentView(binding.root)
setToolbarAsBack(getString(R.string.text_manage_key_store))
keyStoreViewModel =
ViewModelProvider(this, KeyStoreViewModel.Factory(this))[KeyStoreViewModel::class.java]
binding.fab.setOnClickListener {
NewKeyStoreDialog(newKeyStoreDialogCallback).show(supportFragmentManager, null)
}
keyStoreAdapter = KeyStoreAdaptor(keyStoreAdapterCallback)
binding.recyclerView.apply {
adapter = keyStoreAdapter
layoutManager = LinearLayoutManager(this@ManageKeyStoreActivity)
itemAnimator = DefaultItemAnimator()
}
binding.swipeRefreshLayout.setOnRefreshListener {
loadKeyStores()
binding.recyclerView.postDelayed({
binding.swipeRefreshLayout.isRefreshing = false
}, 800)
}
keyStoreViewModel.allKeyStores.observe(this@ManageKeyStoreActivity) {
keyStoreAdapter.submitList(it.toList())
}
loadKeyStores()
}
override fun onResume() {
super.onResume()
loadKeyStores()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_manage_key_store, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_delete_all -> {
MaterialDialog.Builder(this@ManageKeyStoreActivity)
.title(getString(R.string.text_delete_all))
.positiveText(R.string.text_ok).negativeText(R.string.text_cancel)
.onPositive { _: MaterialDialog, _: DialogAction ->
deleteAllKeyStores()
}.show()
}
else -> {}
}
return super.onOptionsItemSelected(item)
}
private fun loadKeyStores() {
val path = File(Pref.getKeyStorePath())
if (!path.isDirectory) {
return
}
val filteredFiles = path.listFiles { _, name ->
name.endsWith(".bks") || name.endsWith(".jks")
} ?: emptyArray()
keyStoreViewModel.updateAllKeyStoresFromFiles(filteredFiles)
}
fun createKeyStore(configs: NewKeyStoreConfigs) {
val keyStorePath = File(Pref.getKeyStorePath())
keyStorePath.mkdirs()
val file = File(keyStorePath, configs.filename)
val distinguishedNameValues = DistinguishedNameValues().apply {
setCommonName(configs.firstAndLastName)
setOrganization(configs.organization)
setOrganizationalUnit(configs.organizationalUnit)
setCountry(configs.countryCode)
setState(configs.stateOrProvince)
setLocality(configs.cityOrLocality)
setStreet(configs.street)
}
try {
CertCreator.createKeystoreAndKey(
file,
configs.password.toCharArray(),
"RSA",
2048,
configs.alias,
configs.aliasPassword.toCharArray(),
configs.signatureAlgorithm,
configs.validityYears,
distinguishedNameValues
)
val newKeyStore = KeyStore(
absolutePath = file.absolutePath,
filename = file.name,
password = configs.password,
alias = configs.alias,
aliasPassword = configs.aliasPassword,
verified = true
)
keyStoreViewModel.upsertKeyStore(newKeyStore)
showToast(R.string.text_successfully_created_key_store)
} catch (e: IOException) {
showToast(getString(R.string.text_failed_to_create_key_store) + " " + e.message)
} catch (e: Exception) {
showToast(getString(R.string.text_failed_to_create_key_store) + " " + e.message)
}
}
fun deleteKeyStore(keyStore: KeyStore) {
val keyStorePath = keyStore.absolutePath
val keyStoreFile = File(keyStorePath)
try {
if (keyStoreFile.delete()) {
keyStoreViewModel.deleteKeyStore(keyStore)
showToast(getString(R.string.text_already_deleted) + " " + keyStore.filename)
} else {
showToast(getString(R.string.text_failed_to_delete))
}
} catch (e: Exception) {
showToast(getString(R.string.text_failed_to_delete) + ": " + e.message)
}
}
private fun deleteAllKeyStores() {
val path = File(Pref.getKeyStorePath())
if (!path.isDirectory) return
val files = path.listFiles { _, name -> name.endsWith(".bks") || name.endsWith(".jks") }
files?.forEach { file ->
file.delete()
}
keyStoreViewModel.deleteAllKeyStores()
showToast(getString(R.string.text_already_deleted))
}
fun verifyKeyStore(
configs: VerifyKeyStoreDialog.VerifyKeyStoreConfigs, keyStore: KeyStore,
) {
// 验证密钥库密码
val tmpKeyStore = try {
KeyStoreHelper.loadKeyStore(File(keyStore.absolutePath), configs.password.toCharArray())
} catch (e: Exception) {
null
}
if (tmpKeyStore == null) {
showToast(R.string.text_verify_failed)
return
}
// 验证别名和别名密码
val tmpKey = try {
tmpKeyStore.getKey(configs.alias, configs.aliasPassword.toCharArray())
} catch (e: Exception) {
null
}
if (tmpKey == null) {
showToast(R.string.text_verify_failed)
return
}
val verifiedKeyStore = KeyStore(
absolutePath = keyStore.absolutePath,
filename = keyStore.filename,
password = configs.password,
alias = configs.alias,
aliasPassword = configs.aliasPassword,
verified = true
)
keyStoreViewModel.upsertKeyStore(verifiedKeyStore)
showToast(R.string.text_verify_success)
}
private fun showToast(@StringRes messageResId: Int) {
Toast.makeText(this, getString(messageResId), Toast.LENGTH_SHORT).show()
}
private fun showToast(message: String) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}
}

View File

@@ -0,0 +1,247 @@
package org.autojs.autojs.ui.keystore
import android.annotation.SuppressLint
import android.app.Dialog
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.ArrayAdapter
import android.widget.LinearLayout
import androidx.fragment.app.DialogFragment
import org.autojs.autojs6.R
import org.autojs.autojs6.databinding.DialogNewKeyStoreBinding
open class NewKeyStoreDialog(
private val callback: Callback,
) : DialogFragment() {
private lateinit var binding: DialogNewKeyStoreBinding
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?,
): View {
binding = DialogNewKeyStoreBinding.inflate(inflater)
return binding.root
}
@SuppressLint("SetTextI18n")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
dialog?.window?.setLayout(
(resources.displayMetrics.widthPixels * 0.85f).toInt(),
LinearLayout.LayoutParams.WRAP_CONTENT
)
dialog?.setCanceledOnTouchOutside(true)
val signatureAlgorithms = arrayOf("MD5withRSA", "SHA1withRSA", "SHA256withRSA", "SHA512withRSA")
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, signatureAlgorithms)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
binding.signatureAlgorithms.adapter = adapter
binding.confirm.setOnClickListener {
var error = false
val filename = binding.filename.text.toString()
val password = binding.password.text.toString()
val alias = binding.alias.text.toString()
val aliasPassword = binding.aliasPassword.text.toString()
var valvalidityYears = 25
// 检查文件名是否符合Android命名规格
when {
filename.isEmpty() -> {
binding.filenameTextInputLayout.error = getString(R.string.text_filename_cannot_be_empty)
error = true
}
!containsSpecialCharacters(filename) -> {
binding.filenameTextInputLayout.error = getString(R.string.text_filename_cannot_contain_invalid_character)
error = true
}
filename.length > 255 -> {
binding.filenameTextInputLayout.error = getString(R.string.text_filename_is_too_long)
error = true
}
else -> binding.filenameTextInputLayout.error = null
}
// 检查密码是否符合要求
when {
password.isEmpty() -> {
binding.passwordTextInputLayout.error = getString(R.string.text_password_cannot_be_empty)
error = true
}
password.length < 6 -> {
binding.passwordTextInputLayout.error = getString(R.string.text_password_requires_at_least_n_characters, 6)
error = true
}
else -> binding.passwordTextInputLayout.error = null
}
// 检查别名密码是否符合要求
when {
aliasPassword.isEmpty() -> {
binding.aliasPasswordTextInputLayout.error = getString(R.string.text_password_cannot_be_empty)
error = true
}
aliasPassword.length < 6 -> {
binding.aliasPasswordTextInputLayout.error = getString(R.string.text_password_requires_at_least_n_characters, 6)
error = true
}
else -> binding.aliasPasswordTextInputLayout.error = null
}
// 检查别名是否符合要求
if (alias.isEmpty()) {
binding.aliasTextInputLayout.error = getString(R.string.text_alias_cannot_be_empty)
error = true
} else {
binding.aliasTextInputLayout.error = null
}
// 检查有效期是否符合要求
if (binding.validityYears.text.toString().isEmpty()) {
binding.validityYearsTextInputLayout.error = getString(R.string.text_validity_years_cannot_be_empty)
error = true
} else {
val years = binding.validityYears.text.toString().toInt()
if (years == 0) {
binding.validityYearsTextInputLayout.error = getString(R.string.text_validity_years_cannot_be_zero)
error = true
} else {
binding.validityYearsTextInputLayout.error = null
valvalidityYears = years
}
}
val firstAndLastName = binding.firstAndLastName.text.toString()
val organization = binding.organization.text.toString()
val organizationalUnit = binding.organizationalUnit.text.toString()
val countryCode = binding.countryCode.text.toString()
val stateOrProvince = binding.stateOrProvince.text.toString()
val cityOrLocality = binding.cityOrLocality.text.toString()
val street = binding.street.text.toString()
if (firstAndLastName.isEmpty() && organization.isEmpty() &&
organizationalUnit.isEmpty() && stateOrProvince.isEmpty() &&
cityOrLocality.isEmpty() && street.isEmpty() && countryCode.isEmpty()
) {
binding.firstAndLastNameTextInputLayout.error = getString(R.string.text_at_least_one_certificate_issuer_field_is_not_empty)
binding.organizationTextInputLayout.error = getString(R.string.text_at_least_one_certificate_issuer_field_is_not_empty)
binding.organizationalUnitTextInputLayout.error = getString(R.string.text_at_least_one_certificate_issuer_field_is_not_empty)
binding.countryCodeTextInputLayout.error = getString(R.string.text_at_least_one_certificate_issuer_field_is_not_empty)
binding.stateOrProvinceTextInputLayout.error = getString(R.string.text_at_least_one_certificate_issuer_field_is_not_empty)
binding.cityOrLocalityTextInputLayout.error = getString(R.string.text_at_least_one_certificate_issuer_field_is_not_empty)
binding.streetTextInputLayout.error = getString(R.string.text_at_least_one_certificate_issuer_field_is_not_empty)
error = true
} else {
binding.firstAndLastNameTextInputLayout.error = null
binding.organizationTextInputLayout.error = null
binding.organizationalUnitTextInputLayout.error = null
binding.countryCodeTextInputLayout.error = null
binding.stateOrProvinceTextInputLayout.error = null
binding.cityOrLocalityTextInputLayout.error = null
binding.streetTextInputLayout.error = null
}
// 检查国家代码是否符合要求 (ISO3166-1-Alpha-2: https://countrycodedata.com/)
val countryCodeRegex = "^[A-Z]{2}$".toRegex()
if (countryCode.isNotEmpty() && !countryCodeRegex.matches(countryCode)) {
binding.countryCodeTextInputLayout.error = getString(R.string.text_country_code_must_be_two_capital_letters)
error = true
}
if (error) return@setOnClickListener
val suffix = getString(
if (binding.typeJks.isChecked) R.string.text_jks
else R.string.text_bks
).lowercase()
val signatureAlgorithm = binding.signatureAlgorithms.selectedItem.toString()
val configs = NewKeyStoreConfigs(
filename = "$filename.$suffix",
password = password,
alias = alias,
aliasPassword = aliasPassword,
signatureAlgorithm = signatureAlgorithm,
validityYears = valvalidityYears,
firstAndLastName = firstAndLastName,
organization = organization,
organizationalUnit = organizationalUnit,
countryCode = countryCode,
stateOrProvince = stateOrProvince,
cityOrLocality = cityOrLocality,
street = street
)
callback.onConfirmButtonClicked(configs)
dismiss()
}
binding.cancel.setOnClickListener {
dismiss()
}
binding.moreOptions.setOnCheckedChangeListener { _, isChecked ->
binding.moreOptionsContainer.visibility = if (isChecked) View.VISIBLE else View.GONE
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return super.onCreateDialog(savedInstanceState).apply {
window?.apply {
setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN)
}
}
}
private fun containsSpecialCharacters(fileName: String): Boolean {
// 定义不允许的字符
val invalidCharacters = listOf("\\", "/", ":", "*", "?", "\"", "<", ">", "|")
// 检查文件名是否包含无效字符
for (char in invalidCharacters) {
if (fileName.contains(char)) {
return false
}
}
return true
}
data class NewKeyStoreConfigs(
val filename: String,
val password: String,
val alias: String,
val aliasPassword: String,
val signatureAlgorithm: String,
val validityYears: Int,
val firstAndLastName: String,
val organizationalUnit: String,
val organization: String,
val countryCode: String,
val stateOrProvince: String,
val cityOrLocality: String,
val street: String,
)
interface Callback {
fun onConfirmButtonClicked(configs: NewKeyStoreConfigs)
}
}

View File

@@ -0,0 +1,133 @@
package org.autojs.autojs.ui.keystore
import android.annotation.SuppressLint
import android.app.Dialog
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.LinearLayout
import androidx.fragment.app.DialogFragment
import org.autojs.autojs.apkbuilder.keystore.KeyStore
import org.autojs.autojs6.R
import org.autojs.autojs6.databinding.DialogVerifyKeyStoreBinding
open class VerifyKeyStoreDialog(
private val callback: Callback,
private val keyStore: KeyStore,
) : DialogFragment() {
private lateinit var binding: DialogVerifyKeyStoreBinding
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?,
): View {
binding = DialogVerifyKeyStoreBinding.inflate(inflater)
return binding.root
}
@SuppressLint("SetTextI18n")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
dialog?.window?.setLayout(
(resources.displayMetrics.widthPixels * 0.85f).toInt(),
LinearLayout.LayoutParams.WRAP_CONTENT
)
dialog?.setCanceledOnTouchOutside(true)
binding.filePath.text = keyStore.absolutePath
if (keyStore.verified) {
binding.imgVerifyState.setImageResource(R.drawable.ic_key_store_verified)
binding.textVerifyState.text = getString(R.string.text_verified)
binding.password.setText(keyStore.password)
binding.alias.setText(keyStore.alias)
binding.aliasPassword.setText(keyStore.aliasPassword)
} else {
binding.imgVerifyState.setImageResource(R.drawable.ic_key_store_unverified)
binding.textVerifyState.text = getString(R.string.text_unverified)
}
binding.verify.setOnClickListener {
var error = false
val password = binding.password.text.toString()
val alias = binding.alias.text.toString()
val aliasPassword = binding.aliasPassword.text.toString()
// 检查密码是否符合要求
when {
password.isEmpty() -> {
binding.passwordTextInputLayout.error = getString(R.string.text_password_cannot_be_empty)
error = true
}
password.length < 6 -> {
binding.passwordTextInputLayout.error = getString(R.string.text_password_requires_at_least_n_characters, 6)
error = true
}
else -> binding.passwordTextInputLayout.error = null
}
// 检查别名密码是否符合要求
when {
aliasPassword.isEmpty() -> {
binding.aliasPasswordTextInputLayout.error = getString(R.string.text_password_cannot_be_empty)
error = true
}
aliasPassword.length < 6 -> {
binding.aliasPasswordTextInputLayout.error = getString(R.string.text_password_requires_at_least_n_characters, 6)
error = true
}
else -> binding.aliasPasswordTextInputLayout.error = null
}
// 检查别名是否符合要求
if (alias.isEmpty()) {
binding.aliasTextInputLayout.error = getString(R.string.text_alias_cannot_be_empty)
error = true
} else {
binding.aliasTextInputLayout.error = null
}
if (error) return@setOnClickListener
val configs = VerifyKeyStoreConfigs(
password = password,
alias = alias,
aliasPassword = aliasPassword,
)
callback.onVerifyButtonClicked(configs, keyStore)
dismiss()
}
binding.cancel.setOnClickListener {
dismiss()
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return super.onCreateDialog(savedInstanceState).apply {
window?.apply {
setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN)
}
}
}
data class VerifyKeyStoreConfigs(
val password: String,
val alias: String,
val aliasPassword: String,
)
interface Callback {
fun onVerifyButtonClicked(configs: VerifyKeyStoreConfigs, keyStore: KeyStore)
}
}

View File

@@ -11,12 +11,16 @@ import android.text.util.Linkify;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.Spinner;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.ViewModelProvider;
import com.afollestad.materialdialogs.MaterialDialog;
import com.google.android.flexbox.FlexboxLayout;
import com.google.android.material.textfield.TextInputLayout;
@@ -34,6 +38,9 @@ import org.autojs.autojs.runtime.api.AppUtils.Companion.SimpleVersionInfo;
import org.autojs.autojs.ui.BaseActivity;
import org.autojs.autojs.ui.common.NotAskAgainDialog;
import org.autojs.autojs.ui.filechooser.FileChooserDialogBuilder;
import org.autojs.autojs.apkbuilder.keystore.KeyStore;
import org.autojs.autojs.ui.viewmodel.KeyStoreViewModel;
import org.autojs.autojs.ui.keystore.ManageKeyStoreActivity;
import org.autojs.autojs.ui.shortcut.AppsIconSelectActivity;
import org.autojs.autojs.ui.widget.RoundCheckboxWithText;
import org.autojs.autojs.util.AndroidUtils;
@@ -109,6 +116,16 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa
put(ApkBuilder.Constants.MLKIT_BARCODE, /* MLKit Barcode */ List.of("barcode", "mlkit-barcode", "mlkit_barcode"));
}};
private static final ArrayList<String> SIGNATURE_SCHEMES = new ArrayList<>() {{
add("V1 + V2");
add("V1 + V3");
add("V1 + V2 + V3");
add("V1");
add("V2 + V3 (Android 7.0+)");
add("V2 (Android 7.0+)");
add("V3 (Android 9.0+)");
}};
EditText mSourcePath;
View mSourcePathContainer;
EditText mOutputPath;
@@ -126,6 +143,8 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa
private boolean mIsProjectLevelBuilding;
private FlexboxLayout mFlexboxAbis;
private FlexboxLayout mFlexboxLibs;
private Spinner mSignatureSchemes;
private Spinner mVerifiedKeyStores;
private final ArrayList<String> mInvalidAbis = new ArrayList<>();
private final ArrayList<String> mUnavailableAbis = new ArrayList<>();
@@ -133,6 +152,8 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa
private final ArrayList<String> mInvalidLibs = new ArrayList<>();
private final ArrayList<String> mUnavailableLibs = new ArrayList<>();
private KeyStoreViewModel mKeyStoreViewModel;
@SuppressLint("SetTextI18n")
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
@@ -195,6 +216,15 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa
mFlexboxLibs = binding.flexboxLibraries;
initLibsChildren();
mKeyStoreViewModel = new ViewModelProvider(this, new KeyStoreViewModel.Factory(getApplicationContext())).get(KeyStoreViewModel.class);
mKeyStoreViewModel.updateVerifiedKeyStores();
mSignatureSchemes = binding.spinnerSignatureSchemes;
initSignatureSchemeSpinner();
mVerifiedKeyStores = binding.spinnerVerifiedKeyStores;
initVerifiedKeyStoresSpinner();
binding.fab.setOnClickListener(v -> buildApk());
binding.selectSource.setOnClickListener(v -> selectSourceFilePath());
binding.selectOutput.setOnClickListener(v -> selectOutputDirPath());
@@ -204,6 +234,7 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa
return true;
});
binding.textLibs.setOnClickListener(v -> toggleAllFlexboxChildren(mFlexboxLibs));
binding.manageKeyStore.setOnClickListener(v -> ManageKeyStoreActivity.Companion.startActivity(this));
setToolbarAsBack(R.string.text_build_apk);
mSource = getIntent().getStringExtra(EXTRA_SOURCE);
@@ -217,6 +248,12 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa
showHintDialogIfNeeded();
}
@Override
protected void onResume() {
super.onResume();
mKeyStoreViewModel.updateVerifiedKeyStores();
}
private void toggleAllFlexboxChildren(FlexboxLayout mFlexboxLibs) {
boolean isAllChecked = true;
for (int i = 0; i < mFlexboxLibs.getChildCount(); i += 1) {
@@ -341,6 +378,32 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa
mInvalidLibs.addAll(candidates);
}
private void initSignatureSchemeSpinner() {
ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, SIGNATURE_SCHEMES);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
mSignatureSchemes.setAdapter(adapter);
}
private void initVerifiedKeyStoresSpinner() {
ArrayList<KeyStore> verifiedKeyStores = new ArrayList<>();
// 添加 默认密钥库 下拉选项
KeyStore defaultKeyStore = new KeyStore("", getString(R.string.text_default_key_store), "", "", "", false); // 仅用于显示下拉列表
verifiedKeyStores.add(defaultKeyStore);
ArrayAdapter<KeyStore> adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, verifiedKeyStores);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
mVerifiedKeyStores.setAdapter(adapter);
mKeyStoreViewModel.getVerifiedKeyStores().observe(this, keyStores -> {
// 清空现有的选项,但保留第一个元素,即默认密钥库
if (verifiedKeyStores.size() > 1) {
verifiedKeyStores.subList(1, verifiedKeyStores.size()).clear();
}
verifiedKeyStores.addAll(keyStores);
adapter.notifyDataSetChanged();
});
}
private boolean isAliasMatching(Map<String, List<String>> aliases, String aliasKey, List<String> candidates) {
AtomicBoolean result = new AtomicBoolean(false);
var aliasList = aliases.getOrDefault(aliasKey, Collections.emptyList());
@@ -632,6 +695,12 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa
appConfig.setAbis(abis);
appConfig.setLibs(libs);
appConfig.setSignatureSchemes(mSignatureSchemes.getSelectedItem().toString());
if (mVerifiedKeyStores.getSelectedItemPosition() > 0) {
appConfig.setKeyStore((KeyStore) mVerifiedKeyStores.getSelectedItem());
} else {
appConfig.setKeyStore(null);
}
return appConfig;
}

View File

@@ -0,0 +1,104 @@
package org.autojs.autojs.ui.viewmodel
import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import org.autojs.autojs.apkbuilder.keystore.KeyStore
import org.autojs.autojs.apkbuilder.keystore.KeyStoreRepository
import java.io.File
class KeyStoreViewModel(context: Context) : ViewModel() {
private val keyStoreRepository: KeyStoreRepository = KeyStoreRepository(context)
private val _allKeyStores = MutableLiveData<List<KeyStore>>()
val allKeyStores: LiveData<List<KeyStore>> get() = _allKeyStores
private val _verifiedKeyStores = MutableLiveData<List<KeyStore>>()
val verifiedKeyStores: LiveData<List<KeyStore>> get() = _verifiedKeyStores
init {
updateVerifiedKeyStores()
}
fun updateVerifiedKeyStores() {
viewModelScope.launch {
val keyStores = keyStoreRepository.getAllKeyStores()
val validKeyStores = mutableListOf<KeyStore>()
keyStores.forEach { keyStore ->
val file = File(keyStore.absolutePath)
if (file.exists()) {
validKeyStores.add(keyStore)
} else {
keyStoreRepository.deleteKeyStores(keyStore)
}
}
_verifiedKeyStores.value = validKeyStores
}
}
fun updateAllKeyStoresFromFiles(files: Array<File>) {
viewModelScope.launch {
val updatedKeyStores = files.map { file ->
keyStoreRepository.getKeyStoreAbsolutePath(file.absolutePath) ?: KeyStore(
absolutePath = file.absolutePath,
filename = file.name
)
}
_allKeyStores.value = updatedKeyStores
}
}
fun upsertKeyStore(keyStore: KeyStore) {
viewModelScope.launch {
keyStoreRepository.upsertKeyStores(keyStore)
val currentKeyStores = _allKeyStores.value ?: emptyList()
val updatedKeyStores =
if (currentKeyStores.any { it.absolutePath == keyStore.absolutePath }) {
currentKeyStores.map {
if (keyStore.absolutePath == it.absolutePath) {
keyStore
} else {
it
}
}
} else {
currentKeyStores + keyStore
}
_allKeyStores.value = updatedKeyStores
}
}
fun deleteKeyStore(keyStore: KeyStore) {
viewModelScope.launch {
keyStoreRepository.deleteKeyStores(keyStore)
val currentKeyStores = _allKeyStores.value ?: emptyList()
val updatedKeyStores = currentKeyStores.filter { it != keyStore }
_allKeyStores.value = updatedKeyStores
}
}
fun deleteAllKeyStores() {
viewModelScope.launch {
keyStoreRepository.deleteAllKeyStores()
_allKeyStores.value = emptyList()
}
}
@Suppress("UNCHECKED_CAST")
class Factory(private val context: Context) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return KeyStoreViewModel(context) as T
}
}
}