diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 933f8797..4ea47673 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -193,6 +193,14 @@ dependencies /* Unclassified */ { // Toaster implementation("com.github.getActivity:Toaster:12.6") implementation("com.github.getActivity:EasyWindow:10.3") + + // apksigner + implementation("com.github.TimScriptov:apksigner:1.2.0") + + // room + implementation("androidx.room:room-runtime:2.6.1") + implementation("androidx.room:room-ktx:2.6.1") + ksp("androidx.room:room-compiler:2.6.1") } dependencies /* MIME */ { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4845d52d..5067b3c6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -256,6 +256,9 @@ + + = emptyList() private set + var keyStore: KeyStore? = null + private set + var signatureSchemes: String = "V1 + V2" + private set + var permissions: List = 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) = also { this.libs = libs } + fun setKeyStore(keyStore: KeyStore?) = also { this.keyStore = keyStore } + + fun setSignatureSchemes(signatureSchemes: String) = also { this.signatureSchemes = signatureSchemes } + + fun setPermissions(permissions: List) = 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) { diff --git a/app/src/main/java/org/autojs/autojs/apkbuilder/ManifestEditor.java b/app/src/main/java/org/autojs/autojs/apkbuilder/ManifestEditor.java index 73e5376b..c89317be 100644 --- a/app/src/main/java/org/autojs/autojs/apkbuilder/ManifestEditor.java +++ b/app/src/main/java/org/autojs/autojs/apkbuilder/ManifestEditor.java @@ -88,6 +88,10 @@ public void onAttr(AxmlWriter.Attr attr) { } } + public boolean isPermissionRequired(String permissionName) { + return true; + } + private class MutableAxmlWriter extends AxmlWriter { private class MutableNodeImpl extends AxmlWriter.NodeImpl { @@ -97,6 +101,9 @@ private class MutableNodeImpl extends AxmlWriter.NodeImpl { @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); } diff --git a/app/src/main/java/org/autojs/autojs/apkbuilder/keystore/AESUtils.kt b/app/src/main/java/org/autojs/autojs/apkbuilder/keystore/AESUtils.kt new file mode 100644 index 00000000..945e7953 --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/apkbuilder/keystore/AESUtils.kt @@ -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) + } +} diff --git a/app/src/main/java/org/autojs/autojs/apkbuilder/keystore/KeyStore.kt b/app/src/main/java/org/autojs/autojs/apkbuilder/keystore/KeyStore.kt new file mode 100644 index 00000000..9174d898 --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/apkbuilder/keystore/KeyStore.kt @@ -0,0 +1,18 @@ +package org.autojs.autojs.apkbuilder.keystore + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + + +@Entity +data class KeyStore( + @PrimaryKey val absolutePath: String, // 密钥库绝对路径 + @ColumnInfo(name = "filename") val filename: String = "", // 文件名 + @ColumnInfo(name = "password") val password: String = "", // 密码 + @ColumnInfo(name = "alias") val alias: String = "", // 别名 + @ColumnInfo(name = "alias_password") val aliasPassword: String = "", // 别名密码 + @ColumnInfo(name = "verified") val verified: Boolean = false, // 验证状态 +) { + override fun toString(): String = filename +} diff --git a/app/src/main/java/org/autojs/autojs/apkbuilder/keystore/KeyStoreDao.kt b/app/src/main/java/org/autojs/autojs/apkbuilder/keystore/KeyStoreDao.kt new file mode 100644 index 00000000..6fbb5008 --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/apkbuilder/keystore/KeyStoreDao.kt @@ -0,0 +1,29 @@ +package org.autojs.autojs.apkbuilder.keystore + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Query +import androidx.room.Upsert + + +@Dao +interface KeyStoreDao { + + @Query("SELECT * FROM keystore WHERE absolutePath = :absolutePath LIMIT 1") + suspend fun getByAbsolutePath(absolutePath: String): KeyStore? + + @Upsert + suspend fun upsert(vararg keyStores: KeyStore) + + @Query("SELECT * FROM keystore") + suspend fun getAll(): List + + @Delete + suspend fun delete(vararg keyStores: KeyStore) + + @Query("DELETE FROM keystore WHERE absolutePath = :absolutePath") + suspend fun deleteByAbsolutePath(absolutePath: String): Int + + @Query("DELETE FROM keystore") + suspend fun deleteAll() +} diff --git a/app/src/main/java/org/autojs/autojs/apkbuilder/keystore/KeyStoreDatabase.kt b/app/src/main/java/org/autojs/autojs/apkbuilder/keystore/KeyStoreDatabase.kt new file mode 100644 index 00000000..d6be0605 --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/apkbuilder/keystore/KeyStoreDatabase.kt @@ -0,0 +1,28 @@ +package org.autojs.autojs.apkbuilder.keystore + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase + +@Database(entities = [KeyStore::class], version = 1, exportSchema = false) +abstract class KeyStoreDatabase : RoomDatabase() { + abstract fun keyStoreDao(): KeyStoreDao + + companion object { + @Volatile + private var INSTANCE: KeyStoreDatabase? = null + + fun getDatabase(context: Context): KeyStoreDatabase { + return INSTANCE ?: synchronized(this) { + val instance = Room.databaseBuilder( + context.applicationContext, + KeyStoreDatabase::class.java, + "keystore-database" + ).build() + INSTANCE = instance + instance + } + } + } +} diff --git a/app/src/main/java/org/autojs/autojs/apkbuilder/keystore/KeyStoreRepository.kt b/app/src/main/java/org/autojs/autojs/apkbuilder/keystore/KeyStoreRepository.kt new file mode 100644 index 00000000..937d5ff8 --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/apkbuilder/keystore/KeyStoreRepository.kt @@ -0,0 +1,50 @@ +package org.autojs.autojs.apkbuilder.keystore + +import android.content.Context +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class KeyStoreRepository(context: Context) { + + private var dao: KeyStoreDao + + init { + val keyStoreDatabase = KeyStoreDatabase.getDatabase(context) + dao = keyStoreDatabase.keyStoreDao() + } + + // 获取所有 KeyStore + suspend fun getAllKeyStores(): List { + return withContext(Dispatchers.IO) { + dao.getAll() + } + } + + // 插入或更新 KeyStore + suspend fun upsertKeyStores(vararg keyStores: KeyStore) { + withContext(Dispatchers.IO) { + dao.upsert(*keyStores) + } + } + + // 根据绝对路径获取 KeyStore + suspend fun getKeyStoreAbsolutePath(absolutePath: String): KeyStore? { + return withContext(Dispatchers.IO) { + dao.getByAbsolutePath(absolutePath) + } + } + + // 删除 KeyStore + suspend fun deleteKeyStores(vararg keyStores: KeyStore) { + withContext(Dispatchers.IO) { + dao.delete(*keyStores) + } + } + + // 删除所有 KeyStore + suspend fun deleteAllKeyStores() { + withContext(Dispatchers.IO) { + dao.deleteAll() + } + } +} diff --git a/app/src/main/java/org/autojs/autojs/core/console/ConsoleView.java b/app/src/main/java/org/autojs/autojs/core/console/ConsoleView.java index 76181da0..2b447af6 100644 --- a/app/src/main/java/org/autojs/autojs/core/console/ConsoleView.java +++ b/app/src/main/java/org/autojs/autojs/core/console/ConsoleView.java @@ -178,7 +178,7 @@ public float getTextSize() { return /* default text size */ 14; } - public void setTextColors(@NotNull Integer[] colors) { + public void setTextColors(Integer[] colors) { Adapter adapter = (Adapter) mLogListRecyclerView.getAdapter(); if (adapter != null) { adapter.setTextColors(colors); diff --git a/app/src/main/java/org/autojs/autojs/core/pref/Pref.kt b/app/src/main/java/org/autojs/autojs/core/pref/Pref.kt index b2be4c82..7fe436b7 100644 --- a/app/src/main/java/org/autojs/autojs/core/pref/Pref.kt +++ b/app/src/main/java/org/autojs/autojs/core/pref/Pref.kt @@ -172,6 +172,11 @@ object Pref { @ScriptInterfaceCompatible fun getScriptDirPath() = WorkingDirectoryUtils.path + @JvmStatic + fun getKeyStorePath(): String { + return getScriptDirPath() + "/.KeyStore/" + } + @JvmStatic fun registerOnSharedPreferenceChangeListener(listener: OnSharedPreferenceChangeListener) { sPref.registerOnSharedPreferenceChangeListener(listener) diff --git a/app/src/main/java/org/autojs/autojs/ui/keystore/KeyStoreAdaptor.kt b/app/src/main/java/org/autojs/autojs/ui/keystore/KeyStoreAdaptor.kt new file mode 100644 index 00000000..ffd58fcb --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/ui/keystore/KeyStoreAdaptor.kt @@ -0,0 +1,82 @@ +package org.autojs.autojs.ui.keystore + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import org.autojs.autojs.apkbuilder.keystore.KeyStore +import org.autojs.autojs6.R +import org.autojs.autojs6.databinding.ItemKeyStoreBinding + +class KeyStoreAdaptor( + private val keyStoreAdapterCallback: KeyStoreAdapterCallback, +) : ListAdapter(KeyStoreDiffCallback()) { + + class KeyStoreDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: KeyStore, newItem: KeyStore): Boolean { + return oldItem.absolutePath == newItem.absolutePath + } + + override fun areContentsTheSame(oldItem: KeyStore, newItem: KeyStore): Boolean { + return oldItem.filename == newItem.filename && + oldItem.password == newItem.password && + oldItem.alias == newItem.alias && + oldItem.aliasPassword == newItem.aliasPassword && + oldItem.verified == newItem.verified + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): KeyStoreViewHolder { + val binding = ItemKeyStoreBinding.inflate( + LayoutInflater.from(parent.context), parent, false + ) + return KeyStoreViewHolder(binding).apply { + binding.delete.setOnClickListener { + if (bindingAdapterPosition != RecyclerView.NO_POSITION) { + keyStoreAdapterCallback.onDeleteButtonClicked(getItem(bindingAdapterPosition)) + } + } + binding.verify.setOnClickListener { + if (bindingAdapterPosition != RecyclerView.NO_POSITION) { + keyStoreAdapterCallback.onVerifyButtonClicked(getItem(bindingAdapterPosition)) + } + } + itemView.setOnClickListener { + if (bindingAdapterPosition != RecyclerView.NO_POSITION) { + keyStoreAdapterCallback.onVerifyButtonClicked(getItem(bindingAdapterPosition)) + } + } + } + } + + override fun onBindViewHolder(holder: KeyStoreViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + inner class KeyStoreViewHolder(private val binding: ItemKeyStoreBinding) : + RecyclerView.ViewHolder(binding.root) { + fun bind(item: KeyStore) { + binding.apply { + filename.text = itemView.context.getString( + R.string.text_str_colon_space_str_formatter, + itemView.context.getString(R.string.text_file_name), + item.filename + ) + alias.text = itemView.context.getString( + R.string.text_str_colon_space_str_formatter, + itemView.context.getString(R.string.text_key_alias), + item.alias + ) + verify.setImageResource( + if (item.verified) R.drawable.ic_key_store_verified else R.drawable.ic_key_store_unverified + ) + } + } + } + + interface KeyStoreAdapterCallback { + fun onDeleteButtonClicked(keyStore: KeyStore) + fun onVerifyButtonClicked(keyStore: KeyStore) + } +} diff --git a/app/src/main/java/org/autojs/autojs/ui/keystore/ManageKeyStoreActivity.kt b/app/src/main/java/org/autojs/autojs/ui/keystore/ManageKeyStoreActivity.kt new file mode 100644 index 00000000..66a6e9f9 --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/ui/keystore/ManageKeyStoreActivity.kt @@ -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() + } +} diff --git a/app/src/main/java/org/autojs/autojs/ui/keystore/NewKeyStoreDialog.kt b/app/src/main/java/org/autojs/autojs/ui/keystore/NewKeyStoreDialog.kt new file mode 100644 index 00000000..35616908 --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/ui/keystore/NewKeyStoreDialog.kt @@ -0,0 +1,247 @@ +package org.autojs.autojs.ui.keystore + +import android.annotation.SuppressLint +import android.app.Dialog +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import android.widget.ArrayAdapter +import android.widget.LinearLayout +import androidx.fragment.app.DialogFragment +import org.autojs.autojs6.R +import org.autojs.autojs6.databinding.DialogNewKeyStoreBinding + +open class NewKeyStoreDialog( + private val callback: Callback, +) : DialogFragment() { + + private lateinit var binding: DialogNewKeyStoreBinding + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, + ): View { + binding = DialogNewKeyStoreBinding.inflate(inflater) + return binding.root + } + + @SuppressLint("SetTextI18n") + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + dialog?.window?.setLayout( + (resources.displayMetrics.widthPixels * 0.85f).toInt(), + LinearLayout.LayoutParams.WRAP_CONTENT + ) + dialog?.setCanceledOnTouchOutside(true) + + val signatureAlgorithms = arrayOf("MD5withRSA", "SHA1withRSA", "SHA256withRSA", "SHA512withRSA") + val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, signatureAlgorithms) + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + binding.signatureAlgorithms.adapter = adapter + + binding.confirm.setOnClickListener { + var error = false + val filename = binding.filename.text.toString() + val password = binding.password.text.toString() + val alias = binding.alias.text.toString() + val aliasPassword = binding.aliasPassword.text.toString() + var valvalidityYears = 25 + + // 检查文件名是否符合Android命名规格 + when { + filename.isEmpty() -> { + binding.filenameTextInputLayout.error = getString(R.string.text_filename_cannot_be_empty) + error = true + } + + !containsSpecialCharacters(filename) -> { + binding.filenameTextInputLayout.error = getString(R.string.text_filename_cannot_contain_invalid_character) + error = true + } + + filename.length > 255 -> { + binding.filenameTextInputLayout.error = getString(R.string.text_filename_is_too_long) + error = true + } + + else -> binding.filenameTextInputLayout.error = null + } + + // 检查密码是否符合要求 + when { + password.isEmpty() -> { + binding.passwordTextInputLayout.error = getString(R.string.text_password_cannot_be_empty) + error = true + } + + password.length < 6 -> { + binding.passwordTextInputLayout.error = getString(R.string.text_password_requires_at_least_n_characters, 6) + error = true + } + + else -> binding.passwordTextInputLayout.error = null + } + + // 检查别名密码是否符合要求 + when { + aliasPassword.isEmpty() -> { + binding.aliasPasswordTextInputLayout.error = getString(R.string.text_password_cannot_be_empty) + error = true + } + + aliasPassword.length < 6 -> { + binding.aliasPasswordTextInputLayout.error = getString(R.string.text_password_requires_at_least_n_characters, 6) + error = true + } + + else -> binding.aliasPasswordTextInputLayout.error = null + } + + // 检查别名是否符合要求 + if (alias.isEmpty()) { + binding.aliasTextInputLayout.error = getString(R.string.text_alias_cannot_be_empty) + error = true + } else { + binding.aliasTextInputLayout.error = null + } + + // 检查有效期是否符合要求 + if (binding.validityYears.text.toString().isEmpty()) { + binding.validityYearsTextInputLayout.error = getString(R.string.text_validity_years_cannot_be_empty) + error = true + } else { + val years = binding.validityYears.text.toString().toInt() + if (years == 0) { + binding.validityYearsTextInputLayout.error = getString(R.string.text_validity_years_cannot_be_zero) + error = true + } else { + binding.validityYearsTextInputLayout.error = null + valvalidityYears = years + } + } + + + val firstAndLastName = binding.firstAndLastName.text.toString() + + val organization = binding.organization.text.toString() + val organizationalUnit = binding.organizationalUnit.text.toString() + + val countryCode = binding.countryCode.text.toString() + val stateOrProvince = binding.stateOrProvince.text.toString() + val cityOrLocality = binding.cityOrLocality.text.toString() + val street = binding.street.text.toString() + + if (firstAndLastName.isEmpty() && organization.isEmpty() && + organizationalUnit.isEmpty() && stateOrProvince.isEmpty() && + cityOrLocality.isEmpty() && street.isEmpty() && countryCode.isEmpty() + ) { + binding.firstAndLastNameTextInputLayout.error = getString(R.string.text_at_least_one_certificate_issuer_field_is_not_empty) + binding.organizationTextInputLayout.error = getString(R.string.text_at_least_one_certificate_issuer_field_is_not_empty) + binding.organizationalUnitTextInputLayout.error = getString(R.string.text_at_least_one_certificate_issuer_field_is_not_empty) + binding.countryCodeTextInputLayout.error = getString(R.string.text_at_least_one_certificate_issuer_field_is_not_empty) + binding.stateOrProvinceTextInputLayout.error = getString(R.string.text_at_least_one_certificate_issuer_field_is_not_empty) + binding.cityOrLocalityTextInputLayout.error = getString(R.string.text_at_least_one_certificate_issuer_field_is_not_empty) + binding.streetTextInputLayout.error = getString(R.string.text_at_least_one_certificate_issuer_field_is_not_empty) + error = true + } else { + binding.firstAndLastNameTextInputLayout.error = null + binding.organizationTextInputLayout.error = null + binding.organizationalUnitTextInputLayout.error = null + binding.countryCodeTextInputLayout.error = null + binding.stateOrProvinceTextInputLayout.error = null + binding.cityOrLocalityTextInputLayout.error = null + binding.streetTextInputLayout.error = null + } + + // 检查国家代码是否符合要求 (ISO3166-1-Alpha-2: https://countrycodedata.com/) + val countryCodeRegex = "^[A-Z]{2}$".toRegex() + if (countryCode.isNotEmpty() && !countryCodeRegex.matches(countryCode)) { + binding.countryCodeTextInputLayout.error = getString(R.string.text_country_code_must_be_two_capital_letters) + error = true + } + + if (error) return@setOnClickListener + + val suffix = getString( + if (binding.typeJks.isChecked) R.string.text_jks + else R.string.text_bks + ).lowercase() + + val signatureAlgorithm = binding.signatureAlgorithms.selectedItem.toString() + + val configs = NewKeyStoreConfigs( + filename = "$filename.$suffix", + password = password, + alias = alias, + aliasPassword = aliasPassword, + signatureAlgorithm = signatureAlgorithm, + validityYears = valvalidityYears, + firstAndLastName = firstAndLastName, + organization = organization, + organizationalUnit = organizationalUnit, + countryCode = countryCode, + stateOrProvince = stateOrProvince, + cityOrLocality = cityOrLocality, + street = street + ) + callback.onConfirmButtonClicked(configs) + dismiss() + } + + binding.cancel.setOnClickListener { + dismiss() + } + + binding.moreOptions.setOnCheckedChangeListener { _, isChecked -> + binding.moreOptionsContainer.visibility = if (isChecked) View.VISIBLE else View.GONE + } + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return super.onCreateDialog(savedInstanceState).apply { + window?.apply { + setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN) + } + } + } + + private fun containsSpecialCharacters(fileName: String): Boolean { + // 定义不允许的字符 + val invalidCharacters = listOf("\\", "/", ":", "*", "?", "\"", "<", ">", "|") + + // 检查文件名是否包含无效字符 + for (char in invalidCharacters) { + if (fileName.contains(char)) { + return false + } + } + + return true + } + + data class NewKeyStoreConfigs( + val filename: String, + val password: String, + val alias: String, + val aliasPassword: String, + val signatureAlgorithm: String, + val validityYears: Int, + val firstAndLastName: String, + val organizationalUnit: String, + val organization: String, + val countryCode: String, + val stateOrProvince: String, + val cityOrLocality: String, + val street: String, + ) + + + interface Callback { + fun onConfirmButtonClicked(configs: NewKeyStoreConfigs) + } +} + diff --git a/app/src/main/java/org/autojs/autojs/ui/keystore/VerifyKeyStoreDialog.kt b/app/src/main/java/org/autojs/autojs/ui/keystore/VerifyKeyStoreDialog.kt new file mode 100644 index 00000000..ef361a53 --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/ui/keystore/VerifyKeyStoreDialog.kt @@ -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) + } +} + diff --git a/app/src/main/java/org/autojs/autojs/ui/project/BuildActivity.java b/app/src/main/java/org/autojs/autojs/ui/project/BuildActivity.java index 6592dc0b..09ea7065 100644 --- a/app/src/main/java/org/autojs/autojs/ui/project/BuildActivity.java +++ b/app/src/main/java/org/autojs/autojs/ui/project/BuildActivity.java @@ -9,14 +9,20 @@ 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; @@ -34,6 +40,9 @@ 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,7 @@ 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 +119,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 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 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; @@ -126,6 +191,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 mInvalidAbis = new ArrayList<>(); private final ArrayList mUnavailableAbis = new ArrayList<>(); @@ -133,6 +201,8 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa private final ArrayList mInvalidLibs = new ArrayList<>(); private final ArrayList mUnavailableLibs = new ArrayList<>(); + private KeyStoreViewModel mKeyStoreViewModel; + @SuppressLint("SetTextI18n") @Override protected void onCreate(@Nullable Bundle savedInstanceState) { @@ -195,6 +265,18 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { 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()); @@ -204,6 +286,8 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { 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); @@ -217,6 +301,12 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { 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) { @@ -229,6 +319,14 @@ private void toggleAllFlexboxChildren(FlexboxLayout mFlexboxLibs) { 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) { @@ -238,6 +336,11 @@ private void toggleAllFlexboxChildren(FlexboxLayout mFlexboxLibs) { continue; } ((RoundCheckboxWithText) child).setChecked(!isAllChecked); + } else if (child instanceof CheckBox) { + if (!child.isEnabled()) { + continue; + } + ((CheckBox) child).setChecked(!isAllChecked); } } } @@ -341,6 +444,51 @@ private void syncLibsCheckedStates() { mInvalidLibs.addAll(candidates); } + private void initSignatureSchemeSpinner() { + ArrayAdapter adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, SIGNATURE_SCHEMES); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + mSignatureSchemes.setAdapter(adapter); + } + + private void initVerifiedKeyStoresSpinner() { + ArrayList verifiedKeyStores = new ArrayList<>(); + // 添加 默认密钥库 下拉选项 + KeyStore defaultKeyStore = new KeyStore("", getString(R.string.text_default_key_store), "", "", "", false); // 仅用于显示下拉列表 + verifiedKeyStores.add(defaultKeyStore); + + ArrayAdapter adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, verifiedKeyStores); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + mVerifiedKeyStores.setAdapter(adapter); + + mKeyStoreViewModel.getVerifiedKeyStores().observe(this, keyStores -> { + // 清空现有的选项,但保留第一个元素,即默认密钥库 + if (verifiedKeyStores.size() > 1) { + verifiedKeyStores.subList(1, verifiedKeyStores.size()).clear(); + } + verifiedKeyStores.addAll(keyStores); + adapter.notifyDataSetChanged(); + }); + } + + @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> aliases, String aliasKey, List candidates) { AtomicBoolean result = new AtomicBoolean(false); var aliasList = aliases.getOrDefault(aliasKey, Collections.emptyList()); @@ -619,6 +767,7 @@ private void doBuildingApk() { private ApkBuilder.AppConfig createAppConfig() { ArrayList abis = collectCheckedItems(mFlexboxAbis); ArrayList libs = collectCheckedItems(mFlexboxLibs); + ArrayList permissions = collectCheckedItems(mFlexboxPermissions); ApkBuilder.AppConfig appConfig = mProjectConfig != null ? ApkBuilder.AppConfig.fromProjectConfig(mSource, mProjectConfig) @@ -632,6 +781,13 @@ private ApkBuilder.AppConfig createAppConfig() { 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; } @@ -649,6 +805,13 @@ private ArrayList collectCheckedItems(FlexboxLayout flexboxLayout) { 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; diff --git a/app/src/main/java/org/autojs/autojs/ui/viewmodel/KeyStoreViewModel.kt b/app/src/main/java/org/autojs/autojs/ui/viewmodel/KeyStoreViewModel.kt new file mode 100644 index 00000000..a19a2937 --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/ui/viewmodel/KeyStoreViewModel.kt @@ -0,0 +1,104 @@ +package org.autojs.autojs.ui.viewmodel + +import android.content.Context +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.launch +import org.autojs.autojs.apkbuilder.keystore.KeyStore +import org.autojs.autojs.apkbuilder.keystore.KeyStoreRepository +import java.io.File + +class KeyStoreViewModel(context: Context) : ViewModel() { + private val keyStoreRepository: KeyStoreRepository = KeyStoreRepository(context) + + private val _allKeyStores = MutableLiveData>() + val allKeyStores: LiveData> get() = _allKeyStores + + private val _verifiedKeyStores = MutableLiveData>() + val verifiedKeyStores: LiveData> get() = _verifiedKeyStores + + init { + updateVerifiedKeyStores() + } + + fun updateVerifiedKeyStores() { + viewModelScope.launch { + val keyStores = keyStoreRepository.getAllKeyStores() + val validKeyStores = mutableListOf() + + keyStores.forEach { keyStore -> + val file = File(keyStore.absolutePath) + if (file.exists()) { + validKeyStores.add(keyStore) + } else { + keyStoreRepository.deleteKeyStores(keyStore) + } + } + + _verifiedKeyStores.value = validKeyStores + } + } + + fun updateAllKeyStoresFromFiles(files: Array) { + viewModelScope.launch { + val updatedKeyStores = files.map { file -> + keyStoreRepository.getKeyStoreAbsolutePath(file.absolutePath) ?: KeyStore( + absolutePath = file.absolutePath, + filename = file.name + ) + } + + _allKeyStores.value = updatedKeyStores + } + } + + fun upsertKeyStore(keyStore: KeyStore) { + viewModelScope.launch { + keyStoreRepository.upsertKeyStores(keyStore) + + val currentKeyStores = _allKeyStores.value ?: emptyList() + + val updatedKeyStores = + if (currentKeyStores.any { it.absolutePath == keyStore.absolutePath }) { + currentKeyStores.map { + if (keyStore.absolutePath == it.absolutePath) { + keyStore + } else { + it + } + } + } else { + currentKeyStores + keyStore + } + + _allKeyStores.value = updatedKeyStores + } + } + + fun deleteKeyStore(keyStore: KeyStore) { + viewModelScope.launch { + keyStoreRepository.deleteKeyStores(keyStore) + + val currentKeyStores = _allKeyStores.value ?: emptyList() + val updatedKeyStores = currentKeyStores.filter { it != keyStore } + _allKeyStores.value = updatedKeyStores + } + } + + fun deleteAllKeyStores() { + viewModelScope.launch { + keyStoreRepository.deleteAllKeyStores() + _allKeyStores.value = emptyList() + } + } + + @Suppress("UNCHECKED_CAST") + class Factory(private val context: Context) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return KeyStoreViewModel(context) as T + } + } +} diff --git a/app/src/main/java/pxb/android/axml/AxmlWriter.java b/app/src/main/java/pxb/android/axml/AxmlWriter.java index 0c9f0de5..b814dee1 100644 --- a/app/src/main/java/pxb/android/axml/AxmlWriter.java +++ b/app/src/main/java/pxb/android/axml/AxmlWriter.java @@ -94,6 +94,9 @@ private int prepare() throws IOException { int size = 0; for (NodeImpl first : firsts) { + if (first.ignore) { + continue; + } size += first.prepare(this); } int a = 0; @@ -163,6 +166,9 @@ public byte[] toByteArray() throws IOException { } for (NodeImpl first : firsts) { + if (first.ignore) { + continue; + } first.write(out); } @@ -259,10 +265,11 @@ protected static class NodeImpl extends NodeVisitor { Attr clz; protected List children = new ArrayList(); 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 int prepare(AxmlWriter axmlWriter) { 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 @@ void write(ByteBuffer out) throws IOException { // children for (NodeImpl child : children) { + if (child.ignore) { + continue; + } child.write(out); } diff --git a/app/src/main/res/drawable/ic_delete_all.xml b/app/src/main/res/drawable/ic_delete_all.xml new file mode 100644 index 00000000..76a5b46c --- /dev/null +++ b/app/src/main/res/drawable/ic_delete_all.xml @@ -0,0 +1,19 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_key_store_delete.xml b/app/src/main/res/drawable/ic_key_store_delete.xml new file mode 100644 index 00000000..2d0d9de1 --- /dev/null +++ b/app/src/main/res/drawable/ic_key_store_delete.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_key_store_unverified.xml b/app/src/main/res/drawable/ic_key_store_unverified.xml new file mode 100644 index 00000000..76826422 --- /dev/null +++ b/app/src/main/res/drawable/ic_key_store_unverified.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_key_store_verified.xml b/app/src/main/res/drawable/ic_key_store_verified.xml new file mode 100644 index 00000000..8a8ec804 --- /dev/null +++ b/app/src/main/res/drawable/ic_key_store_verified.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_build.xml b/app/src/main/res/layout/activity_build.xml index 35898030..22f6e0d9 100644 --- a/app/src/main/res/layout/activity_build.xml +++ b/app/src/main/res/layout/activity_build.xml @@ -315,6 +315,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_new_key_store.xml b/app/src/main/res/layout/dialog_new_key_store.xml new file mode 100644 index 00000000..25585129 --- /dev/null +++ b/app/src/main/res/layout/dialog_new_key_store.xml @@ -0,0 +1,317 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +