diff --git a/manager/build.gradle.kts b/manager/build.gradle.kts index 4435a08..e99756a 100644 --- a/manager/build.gradle.kts +++ b/manager/build.gradle.kts @@ -68,19 +68,19 @@ dependencies { implementation(projects.share.android) implementation("androidx.core:core-ktx:1.7.0") - implementation("androidx.activity:activity-compose:1.5.0-alpha03") + implementation("androidx.activity:activity-compose:1.6.0-alpha01") implementation("androidx.compose.material:material-icons-extended:1.1.1") - implementation("androidx.compose.material3:material3:1.0.0-alpha07") + implementation("androidx.compose.material3:material3:1.0.0-alpha08") implementation("androidx.compose.runtime:runtime-livedata:1.1.1") implementation("androidx.compose.ui:ui:1.1.1") implementation("androidx.compose.ui:ui-tooling:1.1.1") - implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.5.0-alpha04") + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.5.0-alpha05") implementation("androidx.navigation:navigation-compose:2.5.0-alpha03") implementation("androidx.preference:preference:1.2.0") - implementation("com.google.accompanist:accompanist-drawablepainter:0.24.2-alpha") - implementation("com.google.accompanist:accompanist-navigation-animation:0.24.2-alpha") - implementation("com.google.accompanist:accompanist-permissions:0.24.2-alpha") - implementation("com.google.accompanist:accompanist-swiperefresh:0.24.2-alpha") + implementation("com.google.accompanist:accompanist-drawablepainter:0.24.5-alpha") + implementation("com.google.accompanist:accompanist-navigation-animation:0.24.5-alpha") + implementation("com.google.accompanist:accompanist-permissions:0.24.5-alpha") + implementation("com.google.accompanist:accompanist-swiperefresh:0.24.5-alpha") implementation("com.google.android.material:material:1.5.0") implementation("dev.rikka.shizuku:api:12.1.0") implementation("dev.rikka.shizuku:provider:12.1.0") diff --git a/manager/src/main/AndroidManifest.xml b/manager/src/main/AndroidManifest.xml index c7c1d1f..7953c1f 100644 --- a/manager/src/main/AndroidManifest.xml +++ b/manager/src/main/AndroidManifest.xml @@ -12,6 +12,7 @@ android:maxSdkVersion="28" /> diff --git a/manager/src/main/java/org/lsposed/lspatch/LSPApplication.kt b/manager/src/main/java/org/lsposed/lspatch/LSPApplication.kt index 5bd563b..7103c22 100644 --- a/manager/src/main/java/org/lsposed/lspatch/LSPApplication.kt +++ b/manager/src/main/java/org/lsposed/lspatch/LSPApplication.kt @@ -1,6 +1,8 @@ package org.lsposed.lspatch import android.app.Application +import android.content.Context +import android.content.SharedPreferences import android.content.pm.PackageManager import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -15,6 +17,9 @@ class LSPApplication : Application() { var shizukuBinderAvalable = false var shizukuGranted by mutableStateOf(false) + lateinit var appContext: Context + lateinit var prefs: SharedPreferences + init { Shizuku.addBinderReceivedListenerSticky { shizukuBinderAvalable = true @@ -26,4 +31,11 @@ class LSPApplication : Application() { } } } + + override fun onCreate() { + super.onCreate() + appContext = applicationContext + appContext.filesDir.mkdir() + prefs = appContext.getSharedPreferences("settings", Context.MODE_PRIVATE) + } } diff --git a/manager/src/main/java/org/lsposed/lspatch/Patcher.kt b/manager/src/main/java/org/lsposed/lspatch/Patcher.kt index eaf3897..09eccc1 100644 --- a/manager/src/main/java/org/lsposed/lspatch/Patcher.kt +++ b/manager/src/main/java/org/lsposed/lspatch/Patcher.kt @@ -7,6 +7,7 @@ import android.os.Environment import android.provider.MediaStore import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import org.lsposed.lspatch.config.MyKeyStore import org.lsposed.patch.LSPatch import org.lsposed.patch.util.Logger import java.io.File @@ -29,7 +30,7 @@ object Patcher { lateinit var outputPath: String fun toStringArray(): Array { - return arrayListOf().run { + return buildList { addAll(apkPaths) add("-o"); add(outputPath) if (debuggable) add("-d") @@ -43,9 +44,10 @@ object Patcher { if (embeddedModules.isNotEmpty()) { add("-m"); addAll(embeddedModules) } - - toTypedArray() - } + if (!MyKeyStore.useDefault) { + addAll(arrayOf("-k", MyKeyStore.file.path, MyKeyStore.password, MyKeyStore.alias,MyKeyStore.aliasPassword)) + } + }.toTypedArray() } } diff --git a/manager/src/main/java/org/lsposed/lspatch/config/MyKeyStore.kt b/manager/src/main/java/org/lsposed/lspatch/config/MyKeyStore.kt new file mode 100644 index 0000000..7a36943 --- /dev/null +++ b/manager/src/main/java/org/lsposed/lspatch/config/MyKeyStore.kt @@ -0,0 +1,54 @@ +package org.lsposed.lspatch.config + +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.lsposed.lspatch.LSPApplication.Companion.appContext +import org.lsposed.lspatch.LSPApplication.Companion.prefs +import java.io.File + +object MyKeyStore { + + val file = File("${appContext.filesDir}/keystore.bks") + + val tmpFile = File("${appContext.filesDir}/keystore.bks.tmp") + + val password: String + get() = prefs.getString("keystore_password", "123456")!! + + val alias: String + get() = prefs.getString("keystore_alias", "key0")!! + + val aliasPassword: String + get() = prefs.getString("keystore_alias_password", "123456")!! + + private var mUseDefault by mutableStateOf(!file.exists()) + val useDefault by derivedStateOf { mUseDefault } + + suspend fun reset() { + withContext(Dispatchers.IO) { + file.delete() + prefs.edit() + .putString("keystore_password", "123456") + .putString("keystore_alias", "key0") + .putString("keystore_alias_password", "123456") + .apply() + mUseDefault = true + } + } + + suspend fun setCustom(password: String, alias: String, aliasPassword: String) { + withContext(Dispatchers.IO) { + tmpFile.renameTo(file) + prefs.edit() + .putString("keystore_password", password) + .putString("keystore_alias", alias) + .putString("keystore_alias_password", aliasPassword) + .apply() + mUseDefault = false + } + } +} diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/page/PageList.kt b/manager/src/main/java/org/lsposed/lspatch/ui/page/PageList.kt index 34928a9..2c1bb01 100644 --- a/manager/src/main/java/org/lsposed/lspatch/ui/page/PageList.kt +++ b/manager/src/main/java/org/lsposed/lspatch/ui/page/PageList.kt @@ -41,7 +41,7 @@ enum class PageList( Settings( iconSelected = Icons.Filled.Settings, iconNotSelected = Icons.Outlined.Settings, - body = {} + body = { SettingsPage() } ), NewPatch( body = { NewPatchPage(this) } diff --git a/manager/src/main/java/org/lsposed/lspatch/ui/page/SettingsPage.kt b/manager/src/main/java/org/lsposed/lspatch/ui/page/SettingsPage.kt new file mode 100644 index 0000000..8f0d417 --- /dev/null +++ b/manager/src/main/java/org/lsposed/lspatch/ui/page/SettingsPage.kt @@ -0,0 +1,214 @@ +package org.lsposed.lspatch.ui.page + +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Ballot +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch +import org.lsposed.lspatch.R +import org.lsposed.lspatch.config.MyKeyStore +import org.lsposed.lspatch.ui.component.settings.SettingsItem +import java.io.IOException +import java.security.GeneralSecurityException +import java.security.KeyStore + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SettingsPage() { + Scaffold( + topBar = { TopBar() } + ) { innerPadding -> + Column( + modifier = Modifier + .padding(innerPadding) + .verticalScroll(rememberScrollState()) + ) { + KeyStore() + } + } +} + +@Composable +private fun TopBar() { + SmallTopAppBar( + title = { Text(stringResource(R.string.page_settings)) } + ) +} + +@Composable +private fun KeyStore() { + val context = LocalContext.current + val scope = rememberCoroutineScope() + var dropDownExpanded by remember { mutableStateOf(false) } + var showDialog by remember { mutableStateOf(false) } + + Box { + SettingsItem( + onClick = { dropDownExpanded = !dropDownExpanded }, + icon = Icons.Outlined.Ballot, + title = stringResource(R.string.settings_keystore), + desc = stringResource(if (MyKeyStore.useDefault) R.string.settings_keystore_default else R.string.settings_keystore_custom) + ) + DropdownMenu(expanded = dropDownExpanded, onDismissRequest = { dropDownExpanded = false }) { + DropdownMenuItem( + text = { Text(stringResource(R.string.settings_keystore_default)) }, + onClick = { + scope.launch { MyKeyStore.reset() } + dropDownExpanded = false + } + ) + DropdownMenuItem( + text = { Text(stringResource(R.string.settings_keystore_custom)) }, + onClick = { showDialog = true } + ) + } + } + + if (showDialog) { + var wrongKeystore by rememberSaveable { mutableStateOf(false) } + var wrongPassword by rememberSaveable { mutableStateOf(false) } + var wrongAliasName by rememberSaveable { mutableStateOf(false) } + var wrongAliasPassword by rememberSaveable { mutableStateOf(false) } + + var path by rememberSaveable { mutableStateOf("") } + var password by rememberSaveable { mutableStateOf("") } + var alias by rememberSaveable { mutableStateOf("") } + var aliasPassword by rememberSaveable { mutableStateOf("") } + + val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> + if (uri == null) return@rememberLauncherForActivityResult + context.contentResolver.openInputStream(uri).use { input -> + MyKeyStore.tmpFile.outputStream().use { output -> + input?.copyTo(output) + } + } + path = uri.path ?: "" + } + + AlertDialog( + onDismissRequest = { dropDownExpanded = false; showDialog = false }, + confirmButton = { + TextButton( + content = { Text(stringResource(android.R.string.ok)) }, + onClick = { + wrongKeystore = false + wrongPassword = false + wrongAliasName = false + wrongAliasPassword = false + + if (path.isEmpty()) { + wrongKeystore = true + return@TextButton + } + val keyStore = KeyStore.getInstance(KeyStore.getDefaultType()) + try { + MyKeyStore.tmpFile.inputStream().use { input -> + keyStore.load(input, password.toCharArray()) + } + } catch (e: IOException) { + wrongKeystore = true + if (e.message == "KeyStore integrity check failed.") { + wrongPassword = true + } + return@TextButton + } + if (!keyStore.containsAlias(alias)) { + wrongAliasName = true + return@TextButton + } + try { + keyStore.getKey(alias, aliasPassword.toCharArray()) + } catch (e: GeneralSecurityException) { + wrongAliasPassword = true + return@TextButton + } + + scope.launch { MyKeyStore.setCustom(password, alias, aliasPassword) } + dropDownExpanded = false + showDialog = false + }) + }, + dismissButton = { + TextButton( + content = { Text(stringResource(android.R.string.cancel)) }, + onClick = { dropDownExpanded = false; showDialog = false } + ) + }, + title = { Text(stringResource(R.string.settings_keystore_dialog_title)) }, + text = { + Column { + val interactionSource = remember { MutableInteractionSource() } + LaunchedEffect(interactionSource) { + interactionSource.interactions.collect { interaction -> + if (interaction is PressInteraction.Release) { + launcher.launch("*/*") + } + } + } + + val wrongText = when { + wrongAliasPassword -> stringResource(R.string.settings_keystore_wrong_keystore) + wrongAliasName -> stringResource(R.string.settings_keystore_wrong_alias) + wrongPassword -> stringResource(R.string.settings_keystore_wrong_password) + wrongKeystore -> stringResource(R.string.settings_keystore_wrong_keystore) + else -> null + } + Text( + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(bottom = 8.dp), + text = wrongText ?: stringResource(R.string.settings_keystore_desc), + color = if (wrongText != null) MaterialTheme.colorScheme.error else Color.Unspecified + ) + + OutlinedTextField( + value = path, + onValueChange = { path = it }, + readOnly = true, + label = { Text(stringResource(R.string.settings_keystore_file)) }, + singleLine = true, + isError = wrongKeystore, + interactionSource = interactionSource + ) + OutlinedTextField( + value = password, + onValueChange = { password = it }, + label = { Text(stringResource(R.string.settings_keystore_password)) }, + singleLine = true, + isError = wrongPassword + ) + OutlinedTextField( + value = alias, + onValueChange = { alias = it }, + label = { Text(stringResource(R.string.settings_keystore_alias)) }, + singleLine = true, + isError = wrongAliasName + ) + OutlinedTextField( + value = aliasPassword, + onValueChange = { aliasPassword = it }, + label = { Text(stringResource(R.string.settings_keystore_alias_password)) }, + singleLine = true, + isError = wrongAliasPassword + ) + } + } + ) + } +} diff --git a/manager/src/main/res/values/strings.xml b/manager/src/main/res/values/strings.xml index ebee0dc..43872c3 100644 --- a/manager/src/main/res/values/strings.xml +++ b/manager/src/main/res/values/strings.xml @@ -3,7 +3,6 @@ Repo Logs - Settings Shizuku service available @@ -48,4 +47,20 @@ Select Apps + + + Settings + Signature keystore + Built-in + Custom + Custom keystore + Keystore file + Password + Alias + Alias password + Set keystore (BKS) + Wrong type of keystore + Wrong keystore password + Wrong alias name + Wrong alias password