diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 933f8797..4ea47673 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -193,6 +193,14 @@ dependencies /* Unclassified */ {
// Toaster
implementation("com.github.getActivity:Toaster:12.6")
implementation("com.github.getActivity:EasyWindow:10.3")
+
+ // apksigner
+ implementation("com.github.TimScriptov:apksigner:1.2.0")
+
+ // room
+ implementation("androidx.room:room-runtime:2.6.1")
+ implementation("androidx.room:room-ktx:2.6.1")
+ ksp("androidx.room:room-compiler:2.6.1")
}
dependencies /* MIME */ {
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4845d52d..5067b3c6 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -256,6 +256,9 @@
+
+
= 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) = 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()
diff --git a/app/src/main/java/org/autojs/autojs/apkbuilder/keystore/KeyStore.kt b/app/src/main/java/org/autojs/autojs/apkbuilder/keystore/KeyStore.kt
new file mode 100644
index 00000000..9174d898
--- /dev/null
+++ b/app/src/main/java/org/autojs/autojs/apkbuilder/keystore/KeyStore.kt
@@ -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
+}
diff --git a/app/src/main/java/org/autojs/autojs/apkbuilder/keystore/KeyStoreDao.kt b/app/src/main/java/org/autojs/autojs/apkbuilder/keystore/KeyStoreDao.kt
new file mode 100644
index 00000000..6fbb5008
--- /dev/null
+++ b/app/src/main/java/org/autojs/autojs/apkbuilder/keystore/KeyStoreDao.kt
@@ -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
+
+ @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()
+}
diff --git a/app/src/main/java/org/autojs/autojs/apkbuilder/keystore/KeyStoreDatabase.kt b/app/src/main/java/org/autojs/autojs/apkbuilder/keystore/KeyStoreDatabase.kt
new file mode 100644
index 00000000..d6be0605
--- /dev/null
+++ b/app/src/main/java/org/autojs/autojs/apkbuilder/keystore/KeyStoreDatabase.kt
@@ -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
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/org/autojs/autojs/apkbuilder/keystore/KeyStoreRepository.kt b/app/src/main/java/org/autojs/autojs/apkbuilder/keystore/KeyStoreRepository.kt
new file mode 100644
index 00000000..937d5ff8
--- /dev/null
+++ b/app/src/main/java/org/autojs/autojs/apkbuilder/keystore/KeyStoreRepository.kt
@@ -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 {
+ 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()
+ }
+ }
+}
diff --git a/app/src/main/java/org/autojs/autojs/core/console/ConsoleView.java b/app/src/main/java/org/autojs/autojs/core/console/ConsoleView.java
index 76181da0..2b447af6 100644
--- a/app/src/main/java/org/autojs/autojs/core/console/ConsoleView.java
+++ b/app/src/main/java/org/autojs/autojs/core/console/ConsoleView.java
@@ -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);
diff --git a/app/src/main/java/org/autojs/autojs/core/pref/Pref.kt b/app/src/main/java/org/autojs/autojs/core/pref/Pref.kt
index b2be4c82..7fe436b7 100644
--- a/app/src/main/java/org/autojs/autojs/core/pref/Pref.kt
+++ b/app/src/main/java/org/autojs/autojs/core/pref/Pref.kt
@@ -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)
diff --git a/app/src/main/java/org/autojs/autojs/ui/keystore/KeyStoreAdaptor.kt b/app/src/main/java/org/autojs/autojs/ui/keystore/KeyStoreAdaptor.kt
new file mode 100644
index 00000000..ffd58fcb
--- /dev/null
+++ b/app/src/main/java/org/autojs/autojs/ui/keystore/KeyStoreAdaptor.kt
@@ -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(KeyStoreDiffCallback()) {
+
+ class KeyStoreDiffCallback : DiffUtil.ItemCallback() {
+ 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)
+ }
+}
diff --git a/app/src/main/java/org/autojs/autojs/ui/keystore/ManageKeyStoreActivity.kt b/app/src/main/java/org/autojs/autojs/ui/keystore/ManageKeyStoreActivity.kt
new file mode 100644
index 00000000..954d5c86
--- /dev/null
+++ b/app/src/main/java/org/autojs/autojs/ui/keystore/ManageKeyStoreActivity.kt
@@ -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()
+ }
+}
diff --git a/app/src/main/java/org/autojs/autojs/ui/keystore/NewKeyStoreDialog.kt b/app/src/main/java/org/autojs/autojs/ui/keystore/NewKeyStoreDialog.kt
new file mode 100644
index 00000000..35616908
--- /dev/null
+++ b/app/src/main/java/org/autojs/autojs/ui/keystore/NewKeyStoreDialog.kt
@@ -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)
+ }
+}
+
diff --git a/app/src/main/java/org/autojs/autojs/ui/keystore/VerifyKeyStoreDialog.kt b/app/src/main/java/org/autojs/autojs/ui/keystore/VerifyKeyStoreDialog.kt
new file mode 100644
index 00000000..b809161c
--- /dev/null
+++ b/app/src/main/java/org/autojs/autojs/ui/keystore/VerifyKeyStoreDialog.kt
@@ -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)
+ }
+}
+
diff --git a/app/src/main/java/org/autojs/autojs/ui/project/BuildActivity.java b/app/src/main/java/org/autojs/autojs/ui/project/BuildActivity.java
index 6592dc0b..d3d30440 100644
--- a/app/src/main/java/org/autojs/autojs/ui/project/BuildActivity.java
+++ b/app/src/main/java/org/autojs/autojs/ui/project/BuildActivity.java
@@ -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 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 mInvalidAbis = new ArrayList<>();
private final ArrayList mUnavailableAbis = new ArrayList<>();
@@ -133,6 +152,8 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa
private final ArrayList mInvalidLibs = new ArrayList<>();
private final ArrayList 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 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 verifiedKeyStores = new ArrayList<>();
+ // 添加 默认密钥库 下拉选项
+ KeyStore defaultKeyStore = new KeyStore("", getString(R.string.text_default_key_store), "", "", "", false); // 仅用于显示下拉列表
+ verifiedKeyStores.add(defaultKeyStore);
+
+ ArrayAdapter 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> aliases, String aliasKey, List 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;
}
diff --git a/app/src/main/java/org/autojs/autojs/ui/viewmodel/KeyStoreViewModel.kt b/app/src/main/java/org/autojs/autojs/ui/viewmodel/KeyStoreViewModel.kt
new file mode 100644
index 00000000..a19a2937
--- /dev/null
+++ b/app/src/main/java/org/autojs/autojs/ui/viewmodel/KeyStoreViewModel.kt
@@ -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>()
+ val allKeyStores: LiveData> get() = _allKeyStores
+
+ private val _verifiedKeyStores = MutableLiveData>()
+ val verifiedKeyStores: LiveData> get() = _verifiedKeyStores
+
+ init {
+ updateVerifiedKeyStores()
+ }
+
+ fun updateVerifiedKeyStores() {
+ viewModelScope.launch {
+ val keyStores = keyStoreRepository.getAllKeyStores()
+ val validKeyStores = mutableListOf()
+
+ 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) {
+ 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 create(modelClass: Class): T {
+ return KeyStoreViewModel(context) as T
+ }
+ }
+}
diff --git a/app/src/main/res/drawable/ic_delete_all.xml b/app/src/main/res/drawable/ic_delete_all.xml
new file mode 100644
index 00000000..76a5b46c
--- /dev/null
+++ b/app/src/main/res/drawable/ic_delete_all.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_key_store_delete.xml b/app/src/main/res/drawable/ic_key_store_delete.xml
new file mode 100644
index 00000000..2d0d9de1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_key_store_delete.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_key_store_unverified.xml b/app/src/main/res/drawable/ic_key_store_unverified.xml
new file mode 100644
index 00000000..76826422
--- /dev/null
+++ b/app/src/main/res/drawable/ic_key_store_unverified.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_key_store_verified.xml b/app/src/main/res/drawable/ic_key_store_verified.xml
new file mode 100644
index 00000000..8a8ec804
--- /dev/null
+++ b/app/src/main/res/drawable/ic_key_store_verified.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/layout/activity_build.xml b/app/src/main/res/layout/activity_build.xml
index 35898030..18009905 100644
--- a/app/src/main/res/layout/activity_build.xml
+++ b/app/src/main/res/layout/activity_build.xml
@@ -315,6 +315,88 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/dialog_new_key_store.xml b/app/src/main/res/layout/dialog_new_key_store.xml
new file mode 100644
index 00000000..25585129
--- /dev/null
+++ b/app/src/main/res/layout/dialog_new_key_store.xml
@@ -0,0 +1,317 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/dialog_verify_key_store.xml b/app/src/main/res/layout/dialog_verify_key_store.xml
new file mode 100644
index 00000000..5399969f
--- /dev/null
+++ b/app/src/main/res/layout/dialog_verify_key_store.xml
@@ -0,0 +1,163 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_key_store.xml b/app/src/main/res/layout/item_key_store.xml
new file mode 100644
index 00000000..788b3d74
--- /dev/null
+++ b/app/src/main/res/layout/item_key_store.xml
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/menu/menu_manage_key_store.xml b/app/src/main/res/menu/menu_manage_key_store.xml
new file mode 100644
index 00000000..88ebe2c5
--- /dev/null
+++ b/app/src/main/res/menu/menu_manage_key_store.xml
@@ -0,0 +1,9 @@
+
+
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index 70063799..2d73b441 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -759,6 +759,45 @@
ABIs (32 بت)
ABIs (64 بت)
المكتبات
+ تكوين التوقيع
+ مخزن المفاتيح
+ مخزن المفاتيح الافتراضي
+ إدارة مخزن المفاتيح
+ خطة التوقيع
+ إنشاء مفتاح التوقيع
+ كلمة مرور مخزن المفاتيح
+ الاسم المستعار
+ كلمة مرور الاسم المستعار
+ خوارزمية التوقيع
+ المدة (سنوات)
+ الاسم
+ اسم المنظمة
+ وحدة التنظيم
+ رمز الدولة (XX)
+ الولاية أو المقاطعة
+ المدينة أو المنطقة
+ الشارع
+ اسم الملف لا يمكن أن يكون فارغًا
+ اسم الملف لا يمكن أن يحتوي على أي من الأحرف التالية: \\ / : * ? " < > |
+ اسم الملف طويل جدًا
+ يجب أن يحتوي كلمة المرور على %d على الأقل من الأحرف
+ الاسم المستعار لا يمكن أن يكون فارغًا
+ مدة الصلاحية لا يمكن أن تكون فارغة
+ مدة الصلاحية لا يمكن أن تكون صفرًا
+ رمز الدولة يجب أن يكون حرفين كبيرين
+ يجب ملء حقل واحد على الأقل من "الاسم، اسم المنظمة، وحدة التنظيم، رمز الدولة، الولاية أو المقاطعة، المدينة أو المنطقة، الشارع"
+ تم إنشاء مفتاح التوقيع بنجاح
+ فشل في إنشاء مفتاح التوقيع
+ لم يتم التحقق من المخزن
+ تم التحقق من المخزن
+ التحقق
+ حذف الكل
+ لم يتم التحقق
+ تم التحقق
+ التحقق من مخزن المفاتيح
+ تم التحقق بنجاح
+ فشل التحقق
+ مسار الملف
تبديل النافذة
إعدادات الرموز
مطور مصممة خصيصا
diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml
index d12ef96a..f331d8d5 100644
--- a/app/src/main/res/values-en/strings.xml
+++ b/app/src/main/res/values-en/strings.xml
@@ -755,6 +755,45 @@
ABIs (32-bit)
ABIs (64-bit)
Libraries
+ Signature Configuration
+ Keystore
+ Default Keystore
+ Manage Keystore
+ Signature Scheme
+ New Key Store
+ Key Store Password
+ Alias
+ Alias Password
+ Signature Algorithm
+ Validity (Years)
+ Full Name
+ Organization Name
+ Organizational Unit
+ Country Code (XX)
+ State or Province
+ City or Locality
+ Street
+ Filename cannot be empty
+ Filename cannot contain the following characters: \\ / : * ? " < > |
+ Filename is too long
+ Password must be at least %d characters long
+ Alias cannot be empty
+ Validity years cannot be empty
+ Validity years cannot be zero
+ Country code must be two capital letters
+ At least one field from \"Full Name, Organization Name, Organizational Unit, Country Code, State or Province, City or Locality, Street\" must be filled
+ Successfully created signature key
+ Failed to create signature key
+ Keystore has not been verified
+ Keystore has been verified
+ Verify
+ Delete All
+ Unverified
+ Verified
+ Verify Keystore
+ Verification successful
+ Verification failed
+ File Path
Switch window
Symbols settings
Tailor-made developer
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 8f867ffc..b8f6f220 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -758,6 +758,45 @@
ABIs (32 bits)
ABIs (64 bits)
Bibliotecas
+ Configuración de firma
+ Almacenamiento de claves
+ Almacenamiento de claves predeterminado
+ Gestionar almacenamiento de claves
+ Esquema de firma
+ Generar clave de firma
+ Contraseña del almacén de claves
+ Alias
+ Contraseña del alias
+ Algoritmo de firma
+ Validez (años)
+ Nombre completo
+ Nombre de la organización
+ Unidad organizacional
+ Código de país (XX)
+ Estado o provincia
+ Ciudad o localidad
+ Calle
+ El nombre de archivo no puede estar vacío
+ El nombre del archivo no puede contener ninguno de los siguientes caracteres: \\ / : * ? " < > |
+ El nombre del archivo es demasiado largo
+ La contraseña debe tener al menos %d caracteres
+ El alias no puede estar vacío
+ El número de años de validez no puede estar vacío
+ El número de años de validez no puede ser cero
+ El código del país debe ser de dos letras mayúsculas
+ Al menos uno de los campos “Nombre completo, Nombre de la organización, Unidad organizacional, Código de país, Estado o provincia, Ciudad o localidad, Calle” debe estar lleno
+ Clave de firma creada con éxito
+ Error al crear la clave de firma
+ El almacén de claves no ha sido verificado
+ El almacén de claves ha sido verificado
+ Verificar
+ Eliminar todo
+ No verificado
+ Verificado
+ Verificar almacén de claves
+ Verificación exitosa
+ Error en la verificación
+ Ruta del archivo
Cambiar ventana
Config de símbolos
Desarrollador a medida
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 393fa051..aee2036f 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -758,6 +758,45 @@
ABIs (32-bit)
ABIs (64-bit)
Bibliothèques
+ Configuration de la signature
+ Magasin de clés
+ Magasin de clés par défaut
+ Gérer le magasin de clés
+ Schéma de signature
+ Générer une clé de signature
+ Mot de passe du magasin de clés
+ Alias
+ Mot de passe de l\'alias
+ Algorithme de signature
+ Validité (années)
+ Nom complet
+ Nom de l\'organisation
+ Unité organisationnelle
+ Code du pays (XX)
+ État ou province
+ Ville ou localité
+ Rue
+ Le nom de fichier ne peut pas être vide
+ Le nom du fichier ne peut pas contenir les caractères suivants : \\ / : * ? " < > |
+ Le nom du fichier est trop long
+ Le mot de passe doit contenir au moins %d caractères
+ L\'alias ne peut pas être vide
+ Les années de validité ne peuvent pas être vides
+ Les années de validité ne peuvent pas être égales à zéro
+ Le code du pays doit être composé de deux lettres majuscules
+ Au moins un champ parmi « Nom complet, Nom de l\'organisation, Unité organisationnelle, Code du pays, État ou province, Ville ou localité, Rue » doit être renseigné
+ Clé de signature créée avec succès
+ Échec de la création de la clé de signature
+ Le magasin de clés n\'a pas été vérifié
+ Le magasin de clés a été vérifié
+ Vérifier
+ Tout supprimer
+ Non vérifié
+ Vérifié
+ Vérifier le magasin de clés
+ Vérification réussie
+ Échec de la vérification
+ Chemin du fichier
Changer de fenêtre
Param des symboles
Développeur sur mesure
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index 3da880e4..2812f827 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -758,6 +758,45 @@
ABI (32ビット)
ABI (64ビット)
ライブラリー
+ 署名設定
+ キーストア
+ デフォルトのキーストア
+ キーストアを管理
+ 署名スキーム
+ 署名キーを生成
+ キーストアのパスワード
+ エイリアス
+ エイリアスのパスワード
+ 署名アルゴリズム
+ 有効期限(年)
+ 氏名
+ 組織名
+ 組織単位
+ 国コード(XX)
+ 州または省
+ 市区町村
+ 街
+ ファイル名は空にできません
+ ファイル名に以下の文字を含めることはできません:\\ / : * ? " < > |
+ ファイル名が長すぎます
+ パスワードは少なくとも%d文字以上である必要があります
+ エイリアスは空にできません
+ 有効期限の年数は空にできません
+ 有効期限の年数は0にできません
+ 国コードは2文字の大文字でなければなりません
+ 「氏名、組織名、組織単位、国コード、州または省、市区町村、街」のいずれかの項目を入力する必要があります
+ 署名キーが正常に作成されました
+ 署名キーの作成に失敗しました
+ キーストアは確認されていません
+ キーストアは確認されました
+ 確認
+ すべて削除
+ 未確認
+ 確認済み
+ キーストアを確認
+ 確認成功
+ 確認失敗
+ ファイルパス
ウィンドウの切り替え
シンボル設定
テーラーメイド開発者
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
index 46b66fa9..b751a60a 100644
--- a/app/src/main/res/values-ko/strings.xml
+++ b/app/src/main/res/values-ko/strings.xml
@@ -759,6 +759,45 @@
ABI (32 비트)
ABI (64 비트)
라이브러리
+ 서명 설정
+ 키 저장소
+ 기본 키 저장소
+ 키 저장소 관리
+ 서명 스킴
+ 서명 키 생성
+ 키 저장소 비밀번호
+ 별칭
+ 별칭 비밀번호
+ 서명 알고리즘
+ 유효 기간 (년)
+ 성명
+ 조직명
+ 조직 단위
+ 국가 코드 (XX)
+ 주 또는 도
+ 시 또는 지역
+ 거리
+ 파일 이름은 비워둘 수 없습니다
+ 파일 이름에 다음 문자를 포함할 수 없습니다: \\ / : * ? " < > |
+ 파일 이름이 너무 깁니다
+ 비밀번호는 최소 %d자 이상이어야 합니다
+ 별칭은 비워둘 수 없습니다
+ 유효 기간을 입력해야 합니다
+ 유효 기간은 0일 수 없습니다
+ 국가 코드는 두 개의 대문자로 입력해야 합니다
+ "성명, 조직명, 조직 단위, 국가 코드, 주 또는 도, 시 또는 지역, 거리" 중 하나 이상을 입력해야 합니다
+ 서명 키가 성공적으로 생성되었습니다
+ 서명 키 생성에 실패했습니다
+ 키 저장소가 확인되지 않았습니다
+ 키 저장소가 확인되었습니다
+ 확인
+ 모두 삭제
+ 미확인
+ 확인됨
+ 키 저장소 확인
+ 확인 성공
+ 확인 실패
+ 파일 경로
창 전환
기호 설정
맞춤형 개발자
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 98605f99..6a0315f4 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -758,6 +758,45 @@
ABIs (32-bit)
ABIs (64-bit)
Библиотеки
+ Конфигурация подписи
+ Хранилище ключей
+ По умолчанию хранилище ключей
+ Управление хранилищем ключей
+ Схема подписи
+ Создать ключ подписи
+ Пароль хранилища ключей
+ Псевдоним
+ Пароль псевдонима
+ Алгоритм подписи
+ Срок действия (годы)
+ Полное имя
+ Название организации
+ Организационная единица
+ Код страны (XX)
+ Штат или провинция
+ Город или населенный пункт
+ Улица
+ Имя файла не может быть пустым
+ Имя файла не может содержать следующие символы: \\ / : * ? " < > |
+ Имя файла слишком длинное
+ Пароль должен содержать не менее %d символов
+ Псевдоним не может быть пустым
+ Срок действия не может быть пустым
+ Срок действия не может быть равен нулю
+ Код страны должен состоять из двух заглавных букв
+ Необходимо заполнить хотя бы одно поле из: «Полное имя, Название организации, Организационная единица, Код страны, Штат или провинция, Город или населенный пункт, Улица»
+ Ключ подписи успешно создан
+ Не удалось создать ключ подписи
+ Хранилище ключей не подтверждено
+ Хранилище ключей подтверждено
+ Проверить
+ Удалить все
+ Не подтверждено
+ Подтверждено
+ Проверить хранилище ключей
+ Проверка успешна
+ Ошибка проверки
+ Путь к файлу
Переключить окно
Настройки символов
Индивидуальный разработчик
diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml
index 61975e3f..367cf440 100644
--- a/app/src/main/res/values-zh-rHK/strings.xml
+++ b/app/src/main/res/values-zh-rHK/strings.xml
@@ -757,6 +757,45 @@
支持 ABI (32 位)
支持 ABI (64 位)
支援庫
+ 簽名配置
+ 密鑰庫
+ 預設密鑰庫
+ 管理密鑰庫
+ 簽名方案
+ 生成簽名密鑰
+ 密鑰庫密碼
+ 別名
+ 別名密碼
+ 簽名算法
+ 有效期 (年)
+ 姓名
+ 組織名稱
+ 組織單位
+ 國家代碼 (XX)
+ 州或省份
+ 城市或區域
+ 街道
+ 文件名不能為空
+ 文件名不能包含以下字符: \\ / : * ? " < > |
+ 文件名太長
+ 密碼至少需要%d位字符
+ 別名不能為空
+ 有效年份不能為空
+ 有效年份不能為0
+ 國家代碼必須為兩個大寫字母
+ “姓名、組織名稱、組織單位、國家代碼、州或省份、城市或區域、街道”至少填寫一個
+ 成功生成簽名密鑰
+ 生成簽名密鑰失敗
+ 密鑰尚未驗證
+ 密鑰已驗證
+ 驗證
+ 刪除全部
+ 未驗證
+ 已驗證
+ 驗證簽名密鑰
+ 驗證成功
+ 驗證失敗
+ 文件路徑
切換窗口
符號設置
二次開發者
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index 1e12745a..810ab5fb 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -757,6 +757,45 @@
支援 ABI (32 位)
支援 ABI (64 位)
支援庫
+ 簽名設定
+ 密鑰庫
+ 預設密鑰庫
+ 管理密鑰庫
+ 簽名方案
+ 生成簽名密鑰
+ 密鑰庫密碼
+ 別名
+ 別名密碼
+ 簽名演算法
+ 有效期 (年)
+ 姓名
+ 組織名稱
+ 組織單位
+ 國家代碼 (XX)
+ 州或省份
+ 城市或區域
+ 街道
+ 檔案名稱不能為空
+ 檔案名稱不能包含以下字符: \\ / : * ? " < > |
+ 檔案名稱太長
+ 密碼至少需要%d個字元
+ 別名不能為空
+ 有效年份不能為空
+ 有效年份不能為0
+ 國家代碼必須是兩個大寫字母
+ “姓名、組織名稱、組織單位、國家代碼、州或省份、城市或區域、街道”至少填寫一個
+ 成功生成簽名密鑰
+ 生成簽名密鑰失敗
+ 密鑰尚未驗證
+ 密鑰已驗證
+ 驗證
+ 刪除全部
+ 未驗證
+ 已驗證
+ 驗證簽名密鑰
+ 驗證成功
+ 驗證失敗
+ 檔案路徑
切換視窗
符號設定
二次開發者
diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml
index 0e5239ab..ae19f469 100644
--- a/app/src/main/res/values-zh/strings.xml
+++ b/app/src/main/res/values-zh/strings.xml
@@ -753,6 +753,45 @@
支持 ABI (32 位)
支持 ABI (64 位)
支持库
+ 签名配置
+ 密钥库
+ 默认密钥库
+ 管理密钥库
+ 签名方案
+ 生成签名密钥
+ 密钥库密码
+ 别名
+ 别名密码
+ 签名算法
+ 有效期 (年)
+ 姓名
+ 组织名称
+ 组织单位
+ 国家代码 (XX)
+ 州或省份
+ 城市或区域
+ 街道
+ 文件名不能为空
+ 文件名不能包含下列任何字符: \\ / : * ? " < > |
+ 文件名太长
+ 密码至少需要%d位字符
+ 别名不能为空
+ 有效年份不能为空
+ 有效年份不能为0
+ 国家代码必须为两个大写字母
+ “姓名、组织名称、组织单位、国家代码、州或省份、城市或区域、街道”至少填写一个
+ 成功生成签名密钥
+ 生成签名密钥失败
+ 密钥尚未验证
+ 密钥已验证
+ 验证
+ 删除全部
+ 未验证
+ 已验证
+ 验证签名密钥
+ 验证成功
+ 验证失败
+ 文件路径
切换窗口
符号设置
二次开发者
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 89065012..36c69fea 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -966,6 +966,51 @@
ABIs (32-bit)
ABIs (64-bit)
Libraries
+ JKS
+ BKS
+ key0
+ 25
+ CN
+ %s: %s
+ Signature Configuration
+ Keystore
+ Default Keystore
+ Manage Keystore
+ Signature Scheme
+ New Key Store
+ Key Store Password
+ Alias
+ Alias Password
+ Signature Algorithm
+ Validity (Years)
+ Full Name
+ Organization Name
+ Organizational Unit
+ Country Code (XX)
+ State or Province
+ City or Locality
+ Street
+ Filename cannot be empty
+ Filename cannot contain the following characters: \\ / : * ? " < > |
+ Filename is too long
+ Password must be at least %d characters long
+ Alias cannot be empty
+ Validity years cannot be empty
+ Validity years cannot be zero
+ Country code must be two capital letters
+ At least one field from \"Full Name, Organization Name, Organizational Unit, Country Code, State or Province, City or Locality, Street\" must be filled
+ Successfully created signature key
+ Failed to create signature key
+ Keystore has not been verified
+ Keystore has been verified
+ Verify
+ Delete All
+ Unverified
+ Verified
+ Verify Keystore
+ Verification successful
+ Verification failed
+ File Path
Switch window
Symbols settings
Tailor-made developer