新增打包签名配置和密钥库管理
- 打包功能新增签名配置功能,支持设置签名密钥和签名方案(V1、V1 + V2等) - 新增密钥库管理,支持创建和删除自定义签名密钥库等
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user