Merge remote-tracking branch 'origin/master'
# Conflicts: # app/build.gradle.kts # app/src/main/java/org/autojs/autojs/ui/project/BuildActivity.java
This commit is contained in:
@@ -8,7 +8,11 @@ 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.apkbuilder.keystore.AESUtils
|
||||
import org.autojs.autojs.app.GlobalAppContext
|
||||
import org.autojs.autojs.engine.encryption.AdvancedEncryptionStandard
|
||||
import org.autojs.autojs.pio.PFiles
|
||||
@@ -16,6 +20,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 +274,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 = AESUtils.decrypt(it.password)
|
||||
alias = it.alias
|
||||
aliasPassword = AESUtils.decrypt(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 +360,12 @@ 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
|
||||
var permissions: List<String> = emptyList()
|
||||
private set
|
||||
|
||||
fun ignoreDir(dir: File) = also { ignoredDirs.add(dir) }
|
||||
|
||||
@@ -342,6 +387,12 @@ 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 }
|
||||
|
||||
fun setPermissions(permissions: List<String>) = also { this.permissions = permissions }
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun fromProjectConfig(projectDir: String?, projectConfig: ProjectConfig) = AppConfig()
|
||||
@@ -365,6 +416,10 @@ open class ApkBuilder(apkInputStream: InputStream?, private val outApkFile: File
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun isPermissionRequired(permissionName: String): Boolean {
|
||||
return mAppConfig.permissions.contains(permissionName)
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyLibrariesByConfig(config: AppConfig) {
|
||||
|
||||
@@ -88,6 +88,10 @@ public class ManifestEditor {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isPermissionRequired(String permissionName) {
|
||||
return true;
|
||||
}
|
||||
|
||||
private class MutableAxmlWriter extends AxmlWriter {
|
||||
private class MutableNodeImpl extends AxmlWriter.NodeImpl {
|
||||
|
||||
@@ -97,6 +101,9 @@ public class ManifestEditor {
|
||||
|
||||
@Override
|
||||
protected void onAttr(AxmlWriter.Attr a) {
|
||||
if ("uses-permission".equals(this.name.data) && "name".equals(a.name.data) && a.value instanceof StringItem) {
|
||||
this.ignore = !ManifestEditor.this.isPermissionRequired(((StringItem) a.value).data);
|
||||
}
|
||||
ManifestEditor.this.onAttr(a);
|
||||
super.onAttr(a);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package org.autojs.autojs.apkbuilder.keystore
|
||||
|
||||
import android.util.Base64
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.SecretKey
|
||||
import javax.crypto.spec.GCMParameterSpec
|
||||
import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyProperties
|
||||
import java.security.KeyStore
|
||||
import javax.crypto.KeyGenerator
|
||||
|
||||
object AESUtils {
|
||||
|
||||
private const val TRANSFORMATION = "AES/GCM/NoPadding"
|
||||
private const val TAG_LENGTH = 128
|
||||
|
||||
private const val KEY_ALIAS = "autojs6_key_store_aes_key"
|
||||
|
||||
private fun getKey(): SecretKey {
|
||||
val keyStore = KeyStore.getInstance("AndroidKeyStore")
|
||||
keyStore.load(null)
|
||||
|
||||
val key = keyStore.getKey(KEY_ALIAS, null)
|
||||
if (key != null) {
|
||||
return key as SecretKey
|
||||
}
|
||||
|
||||
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
|
||||
val keyGenParameterSpec = KeyGenParameterSpec.Builder(KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
|
||||
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
|
||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
||||
.build()
|
||||
|
||||
keyGenerator.init(keyGenParameterSpec)
|
||||
return keyGenerator.generateKey()
|
||||
}
|
||||
|
||||
// 加密
|
||||
fun encrypt(data: String): String {
|
||||
val secretKey: SecretKey = getKey()
|
||||
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
|
||||
|
||||
val encryptedData = cipher.doFinal(data.toByteArray())
|
||||
val encryptedBase64 = Base64.encodeToString(encryptedData, Base64.NO_WRAP)
|
||||
val ivBase64 = Base64.encodeToString(cipher.iv, Base64.NO_WRAP)
|
||||
|
||||
return "$ivBase64:$encryptedBase64"
|
||||
}
|
||||
|
||||
// 解密
|
||||
fun decrypt(encryptedData: String): String {
|
||||
val parts = encryptedData.split(":")
|
||||
val ivBase64 = parts[0]
|
||||
val encryptedBase64 = parts[1]
|
||||
|
||||
val iv = Base64.decode(ivBase64, Base64.NO_WRAP)
|
||||
val encrypted = Base64.decode(encryptedBase64, Base64.NO_WRAP)
|
||||
|
||||
val secretKey: SecretKey = getKey()
|
||||
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||
|
||||
val gcmSpec = GCMParameterSpec(TAG_LENGTH, iv)
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmSpec)
|
||||
|
||||
val decryptedData = cipher.doFinal(encrypted)
|
||||
|
||||
return String(decryptedData)
|
||||
}
|
||||
}
|
||||
@@ -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,267 @@
|
||||
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.AESUtils
|
||||
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 = AESUtils.encrypt(configs.password),
|
||||
alias = configs.alias,
|
||||
aliasPassword = AESUtils.encrypt(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 = AESUtils.encrypt(configs.password),
|
||||
alias = configs.alias,
|
||||
aliasPassword = AESUtils.encrypt(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,134 @@
|
||||
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.AESUtils
|
||||
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(AESUtils.decrypt(keyStore.password))
|
||||
binding.alias.setText(keyStore.alias)
|
||||
binding.aliasPassword.setText(AESUtils.decrypt(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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,14 +9,20 @@ import android.text.Editable;
|
||||
import android.text.TextUtils;
|
||||
import android.text.util.Linkify;
|
||||
import android.util.Log;
|
||||
import android.view.Gravity;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import android.widget.CheckBox;
|
||||
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;
|
||||
@@ -35,6 +41,9 @@ import org.autojs.autojs.runtime.api.augment.pinyin.Pinyin;
|
||||
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;
|
||||
@@ -56,6 +65,8 @@ import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.TreeMap;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Pattern;
|
||||
@@ -109,6 +120,61 @@ 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+)");
|
||||
}};
|
||||
|
||||
private final Map<String, Integer> SUPPORTED_PERMISSIONS = new TreeMap<>() {{
|
||||
put("android.permission.ACCESS_COARSE_LOCATION", R.string.text_permission_access_coarse_location);
|
||||
put("android.permission.ACCESS_FINE_LOCATION", R.string.text_permission_access_fine_location);
|
||||
put("android.permission.ACCESS_NETWORK_STATE", R.string.text_permission_access_network_state);
|
||||
put("android.permission.ACCESS_WIFI_STATE", R.string.text_permission_access_wifi_state);
|
||||
put("android.permission.BROADCAST_CLOSE_SYSTEM_DIALOGS", R.string.text_permission_broadcast_close_system_dialogs);
|
||||
put("android.permission.CAPTURE_VIDEO_OUTPUT", R.string.text_permission_capture_video_output);
|
||||
put("android.permission.DUMP", R.string.text_permission_dump);
|
||||
put("android.permission.FOREGROUND_SERVICE", R.string.text_permission_foreground_service);
|
||||
put("android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION", R.string.text_permission_foreground_service_media_projection);
|
||||
put("android.permission.FOREGROUND_SERVICE_SPECIAL_USE", R.string.text_permission_foreground_service_special_use);
|
||||
put("android.permission.INTERNET", R.string.text_permission_internet);
|
||||
put("android.permission.INTERACT_ACROSS_USERS_FULL", R.string.text_permission_interact_across_users_full);
|
||||
put("android.permission.MANAGE_EXTERNAL_STORAGE", R.string.text_permission_manage_external_storage);
|
||||
put("android.permission.MANAGE_USERS", R.string.text_permission_manage_users);
|
||||
put("android.permission.POST_NOTIFICATIONS", R.string.text_permission_post_notifications);
|
||||
put("android.permission.QUERY_ALL_PACKAGES", R.string.text_permission_query_all_packages);
|
||||
put("android.permission.READ_EXTERNAL_STORAGE", R.string.text_permission_read_external_storage);
|
||||
put("android.permission.READ_MEDIA_AUDIO", R.string.text_permission_read_media_audio);
|
||||
put("android.permission.READ_MEDIA_IMAGES", R.string.text_permission_read_media_images);
|
||||
put("android.permission.READ_MEDIA_VIDEO", R.string.text_permission_read_media_video);
|
||||
put("android.permission.READ_PHONE_STATE", R.string.text_permission_read_phone_state);
|
||||
put("android.permission.READ_PRIVILEGED_PHONE_STATE", R.string.text_permission_read_privileged_phone_state);
|
||||
put("android.permission.READ_SMS", R.string.text_permission_read_sms);
|
||||
put("android.permission.RECEIVE_BOOT_COMPLETED", R.string.text_permission_receive_boot_completed);
|
||||
put("android.permission.RECORD_AUDIO", R.string.text_permission_record_audio);
|
||||
put("android.permission.REORDER_TASKS", R.string.text_permission_reorder_tasks);
|
||||
put("android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS", R.string.text_permission_request_ignore_battery_optimizations);
|
||||
put("android.permission.REQUEST_INSTALL_PACKAGES", R.string.text_permission_request_install_packages);
|
||||
put("android.permission.SCHEDULE_EXACT_ALARM", R.string.text_permission_schedule_exact_alarm);
|
||||
put("android.permission.SYSTEM_ALERT_WINDOW", R.string.text_permission_system_alert_window);
|
||||
put("android.permission.UNLIMITED_TOASTS", R.string.text_permission_unlimited_toasts);
|
||||
put("android.permission.UNINSTALL_SHORTCUT", R.string.text_permission_uninstall_shortcut);
|
||||
put("android.permission.USE_EXACT_ALARM", R.string.text_permission_use_exact_alarm);
|
||||
put("android.permission.VIBRATE", R.string.text_permission_vibrate);
|
||||
put("android.permission.WAKE_LOCK", R.string.text_permission_wake_lock);
|
||||
put("android.permission.WRITE_EXTERNAL_STORAGE", R.string.text_permission_write_external_storage);
|
||||
put("android.permission.WRITE_SECURE_SETTINGS", R.string.text_permission_write_secure_settings);
|
||||
put("android.permission.WRITE_SETTINGS", R.string.text_permission_write_settings);
|
||||
put("com.android.launcher.permission.INSTALL_SHORTCUT", R.string.text_permission_install_shortcut);
|
||||
put("com.android.launcher.permission.UNINSTALL_SHORTCUT", R.string.text_permission_uninstall_shortcut);
|
||||
put("com.termux.permission.RUN_COMMAND", R.string.text_permission_run_command);
|
||||
put("moe.shizuku.manager.permission.API_V23", R.string.text_permission_shizuku);
|
||||
}};
|
||||
|
||||
EditText mSourcePath;
|
||||
View mSourcePathContainer;
|
||||
EditText mOutputPath;
|
||||
@@ -129,6 +195,9 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa
|
||||
private boolean mIsProjectLevelBuilding;
|
||||
private FlexboxLayout mFlexboxAbis;
|
||||
private FlexboxLayout mFlexboxLibs;
|
||||
private Spinner mSignatureSchemes;
|
||||
private Spinner mVerifiedKeyStores;
|
||||
private FlexboxLayout mFlexboxPermissions;
|
||||
|
||||
private final ArrayList<String> mInvalidAbis = new ArrayList<>();
|
||||
private final ArrayList<String> mUnavailableAbis = new ArrayList<>();
|
||||
@@ -136,6 +205,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) {
|
||||
@@ -202,6 +273,18 @@ 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();
|
||||
|
||||
mFlexboxPermissions = binding.flexboxPermissions;
|
||||
initPermissionsChildren();
|
||||
|
||||
binding.fab.setOnClickListener(v -> buildApk());
|
||||
binding.selectSource.setOnClickListener(v -> selectSourceFilePath());
|
||||
binding.selectOutput.setOnClickListener(v -> selectOutputDirPath());
|
||||
@@ -211,6 +294,8 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa
|
||||
return true;
|
||||
});
|
||||
binding.textLibs.setOnClickListener(v -> toggleAllFlexboxChildren(mFlexboxLibs));
|
||||
binding.manageKeyStore.setOnClickListener(v -> ManageKeyStoreActivity.Companion.startActivity(this));
|
||||
binding.textPermissions.setOnClickListener(v -> toggleAllFlexboxChildren(mFlexboxPermissions));
|
||||
|
||||
setToolbarAsBack(R.string.text_build_apk);
|
||||
mSource = getIntent().getStringExtra(EXTRA_SOURCE);
|
||||
@@ -224,6 +309,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) {
|
||||
@@ -236,6 +327,14 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa
|
||||
isAllChecked = false;
|
||||
break;
|
||||
}
|
||||
} else if (child instanceof CheckBox) {
|
||||
if (!child.isEnabled()) {
|
||||
continue;
|
||||
}
|
||||
if (!((CheckBox) child).isChecked()) {
|
||||
isAllChecked = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < mFlexboxLibs.getChildCount(); i += 1) {
|
||||
@@ -245,6 +344,11 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa
|
||||
continue;
|
||||
}
|
||||
((RoundCheckboxWithText) child).setChecked(!isAllChecked);
|
||||
} else if (child instanceof CheckBox) {
|
||||
if (!child.isEnabled()) {
|
||||
continue;
|
||||
}
|
||||
((CheckBox) child).setChecked(!isAllChecked);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -348,6 +452,51 @@ 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();
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private void initPermissionsChildren() {
|
||||
SUPPORTED_PERMISSIONS.forEach((permission, descriptionResId) -> {
|
||||
CheckBox checkBox = new CheckBox(this);
|
||||
checkBox.setLayoutParams(new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT));
|
||||
checkBox.setAlpha(0.87f);
|
||||
checkBox.setText(permission + "\n" + getString(descriptionResId));
|
||||
checkBox.setButtonDrawable(R.drawable.round_checkbox);
|
||||
checkBox.setGravity(Gravity.CENTER_VERTICAL);
|
||||
checkBox.setTextSize(12);
|
||||
int marginInPixels = (int) (8 * getResources().getDisplayMetrics().density);
|
||||
checkBox.setPadding(marginInPixels, 0, 0, 0);
|
||||
checkBox.setChecked(false);
|
||||
mFlexboxPermissions.addView(checkBox);
|
||||
});
|
||||
}
|
||||
|
||||
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());
|
||||
@@ -650,6 +799,7 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa
|
||||
private ApkBuilder.AppConfig createAppConfig() {
|
||||
ArrayList<String> abis = collectCheckedItems(mFlexboxAbis);
|
||||
ArrayList<String> libs = collectCheckedItems(mFlexboxLibs);
|
||||
ArrayList<String> permissions = collectCheckedItems(mFlexboxPermissions);
|
||||
|
||||
ApkBuilder.AppConfig appConfig = mProjectConfig != null
|
||||
? ApkBuilder.AppConfig.fromProjectConfig(mSource, mProjectConfig)
|
||||
@@ -663,6 +813,13 @@ 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);
|
||||
}
|
||||
appConfig.setPermissions(permissions);
|
||||
|
||||
return appConfig;
|
||||
}
|
||||
@@ -680,6 +837,13 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa
|
||||
libs.add(charSequence.toString());
|
||||
}
|
||||
}
|
||||
} else if (child instanceof CheckBox) {
|
||||
if (((CheckBox) child).isChecked()) {
|
||||
CharSequence charSequence = ((CheckBox) child).getText();
|
||||
if (charSequence != null) {
|
||||
libs.add(charSequence.toString().split("\n")[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return libs;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -94,6 +94,9 @@ public class AxmlWriter extends AxmlVisitor {
|
||||
int size = 0;
|
||||
|
||||
for (NodeImpl first : firsts) {
|
||||
if (first.ignore) {
|
||||
continue;
|
||||
}
|
||||
size += first.prepare(this);
|
||||
}
|
||||
int a = 0;
|
||||
@@ -163,6 +166,9 @@ public class AxmlWriter extends AxmlVisitor {
|
||||
}
|
||||
|
||||
for (NodeImpl first : firsts) {
|
||||
if (first.ignore) {
|
||||
continue;
|
||||
}
|
||||
first.write(out);
|
||||
}
|
||||
|
||||
@@ -259,10 +265,11 @@ public class AxmlWriter extends AxmlVisitor {
|
||||
Attr clz;
|
||||
protected List<NodeImpl> children = new ArrayList<NodeImpl>();
|
||||
private int line;
|
||||
private StringItem name;
|
||||
protected StringItem name;
|
||||
private StringItem ns;
|
||||
private StringItem text;
|
||||
private int textLineNumber;
|
||||
protected boolean ignore = false;
|
||||
|
||||
public NodeImpl(String ns, String name) {
|
||||
super(null);
|
||||
@@ -344,6 +351,9 @@ public class AxmlWriter extends AxmlVisitor {
|
||||
int size = 24 + 36 + attrs.size() * 20;// 24 for end tag,36+x*20 for
|
||||
// start tag
|
||||
for (NodeImpl child : children) {
|
||||
if (child.ignore) {
|
||||
continue;
|
||||
}
|
||||
size += child.prepare(axmlWriter);
|
||||
}
|
||||
if (text != null) {
|
||||
@@ -400,6 +410,9 @@ public class AxmlWriter extends AxmlVisitor {
|
||||
|
||||
// children
|
||||
for (NodeImpl child : children) {
|
||||
if (child.ignore) {
|
||||
continue;
|
||||
}
|
||||
child.write(out);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user