Skip to content

Commit

Permalink
feat: improve keystore UI and UX (ReVanced#52)
Browse files Browse the repository at this point in the history
  • Loading branch information
Axelen123 authored Jul 7, 2023
1 parent 37e177b commit aa02e9f
Show file tree
Hide file tree
Showing 6 changed files with 206 additions and 104 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,24 @@ import java.io.File
import java.io.InputStream
import java.io.OutputStream
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import kotlin.io.path.exists

class KeystoreManager(app: Application, private val prefs: PreferencesManager) {
companion object {
/**
* Default common name and password for the keystore.
* Default alias and password for the keystore.
*/
const val DEFAULT = "ReVanced"

/**
* The default password used by the Flutter version.
*/
const val FLUTTER_MANAGER_PASSWORD = "s3cur3p@ssw0rd"
}

private val keystorePath = app.getDir("signing", Context.MODE_PRIVATE).resolve("manager.keystore").toPath()
private val keystorePath =
app.getDir("signing", Context.MODE_PRIVATE).resolve("manager.keystore").toPath()

private fun options(
cn: String = prefs.keystoreCommonName!!,
pass: String = prefs.keystorePass!!
pass: String = prefs.keystorePass!!,
) = SigningOptions(cn, pass, keystorePath)

private fun updatePrefs(cn: String, pass: String) {
Expand All @@ -47,11 +45,14 @@ class KeystoreManager(app: Application, private val prefs: PreferencesManager) {
updatePrefs(DEFAULT, DEFAULT)
}

fun import(cn: String, pass: String, keystore: InputStream) {
// TODO: check if the user actually provided the correct password
fun import(cn: String, pass: String, keystore: Path): Boolean {
if (!Signer(SigningOptions(cn, pass, keystore)).canUnlock()) {
return false
}
Files.copy(keystore, keystorePath, StandardCopyOption.REPLACE_EXISTING)

updatePrefs(cn, pass)
return true
}

fun export(target: OutputStream) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package app.revanced.manager.ui.component

import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Visibility
import androidx.compose.material.icons.outlined.VisibilityOff
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import app.revanced.manager.R

@Composable
fun PasswordField(modifier: Modifier = Modifier, value: String, onValueChange: (String) -> Unit, label: @Composable (() -> Unit)? = null, placeholder: @Composable (() -> Unit)? = null) {
var visible by rememberSaveable {
mutableStateOf(false)
}

OutlinedTextField(
value = value,
onValueChange = onValueChange,
placeholder = placeholder,
label = label,
modifier = modifier,
trailingIcon = {
IconButton(onClick = {
visible = !visible
}) {
val (icon, description) = remember(visible) {
if (visible) Icons.Outlined.VisibilityOff to R.string.hide_password_field else Icons.Outlined.Visibility to R.string.show_password_field
}
Icon(icon, stringResource(description))
}
},
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password
),
visualTransformation = if (visible) VisualTransformation.None else PasswordVisualTransformation()
)
}
Original file line number Diff line number Diff line change
@@ -1,44 +1,53 @@
package app.revanced.manager.ui.screen.settings

import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
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.Key
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R
import app.revanced.manager.ui.viewmodel.ImportExportViewModel
import app.revanced.manager.domain.manager.KeystoreManager.Companion.DEFAULT
import app.revanced.manager.domain.manager.KeystoreManager.Companion.FLUTTER_MANAGER_PASSWORD
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.ContentSelector
import app.revanced.manager.ui.component.GroupHeader
import app.revanced.manager.ui.component.PasswordField
import app.revanced.manager.ui.component.sources.SourceSelector
import app.revanced.manager.util.toast
import org.koin.androidx.compose.getViewModel
import org.koin.compose.rememberKoinInject

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ImportExportSettingsScreen(
onBackClick: () -> Unit,
vm: ImportExportViewModel = getViewModel()
) {
var showImportKeystoreDialog by rememberSaveable { mutableStateOf(false) }
var showExportKeystoreDialog by rememberSaveable { mutableStateOf(false) }
val importKeystoreLauncher =
rememberLauncherForActivityResult(contract = ActivityResultContracts.GetContent()) {
it?.let { uri -> vm.startKeystoreImport(uri) }
}
val exportKeystoreLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("*/*")) {
it?.let(vm::exportKeystore)
}

vm.selectionAction?.let { action ->
val sources by vm.sources.collectAsStateWithLifecycle(initialValue = emptyList())
Expand All @@ -62,16 +71,10 @@ fun ImportExportSettingsScreen(
}
}

if (showImportKeystoreDialog) {
ImportKeystoreDialog(
onDismissRequest = { showImportKeystoreDialog = false },
onImport = vm::importKeystore
)
}
if (showExportKeystoreDialog) {
ExportKeystoreDialog(
onDismissRequest = { showExportKeystoreDialog = false },
onExport = vm::exportKeystore
if (vm.showCredentialsDialog) {
KeystoreCredentialsDialog(
onDismissRequest = vm::cancelKeystoreImport,
tryImport = vm::tryKeystoreImport
)
}

Expand All @@ -92,14 +95,14 @@ fun ImportExportSettingsScreen(
GroupHeader(stringResource(R.string.signing))
GroupItem(
onClick = {
showImportKeystoreDialog = true
importKeystoreLauncher.launch("*/*")
},
headline = R.string.import_keystore,
description = R.string.import_keystore_descripion
)
GroupItem(
onClick = {
showExportKeystoreDialog = true
exportKeystoreLauncher.launch("Manager.keystore")
},
headline = R.string.export_keystore,
description = R.string.export_keystore_description
Expand Down Expand Up @@ -139,90 +142,64 @@ private fun GroupItem(onClick: () -> Unit, @StringRes headline: Int, @StringRes
)

@Composable
fun ExportKeystoreDialog(
fun KeystoreCredentialsDialog(
onDismissRequest: () -> Unit,
onExport: (Uri) -> Unit
tryImport: (String, String) -> Boolean
) {
val activityLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("*/*")) { uri ->
uri?.let {
onExport(it)
onDismissRequest()
}
}
val prefs: PreferencesManager = rememberKoinInject()
val context = LocalContext.current
var cn by rememberSaveable { mutableStateOf("") }
var pass by rememberSaveable { mutableStateOf("") }

AlertDialog(
onDismissRequest = onDismissRequest,
confirmButton = {
Button(
onClick = { activityLauncher.launch("Manager.keystore") }
TextButton(
onClick = {
if (!tryImport(
cn,
pass
)
) context.toast(context.getString(R.string.import_keystore_wrong_credentials))
}
) {
Text(stringResource(R.string.select_file))
Text(stringResource(R.string.import_keystore_dialog_button))
}
},
title = { Text(stringResource(R.string.export_keystore)) },
text = {
Column {
Text("Current common name: ${prefs.keystoreCommonName}")
Text("Current password: ${prefs.keystorePass}")
}
}
)
}

@Composable
fun ImportKeystoreDialog(
onDismissRequest: () -> Unit, onImport: (Uri, String, String) -> Unit
) {
var cn by rememberSaveable { mutableStateOf(DEFAULT) }
var pass by rememberSaveable { mutableStateOf(DEFAULT) }

AlertDialog(
onDismissRequest = onDismissRequest,
confirmButton = {
ContentSelector(
mime = "*/*",
onSelect = {
onImport(it, cn, pass)
onDismissRequest()
}
) {
Text(stringResource(R.string.select_file))
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(stringResource(R.string.cancel))
}
},
title = { Text(stringResource(R.string.import_keystore)) },
icon = {
Icon(Icons.Outlined.Key, null)
},
title = {
Text(
text = stringResource(R.string.import_keystore_dialog_title),
style = MaterialTheme.typography.headlineSmall.copy(textAlign = TextAlign.Center),
color = MaterialTheme.colorScheme.onSurface,
)
},
text = {
Column {
TextField(
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = stringResource(R.string.import_keystore_dialog_description),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
OutlinedTextField(
value = cn,
onValueChange = { cn = it },
label = { Text("Common Name") }
label = { Text(stringResource(R.string.import_keystore_dialog_alias_field)) }
)
TextField(
PasswordField(
value = pass,
onValueChange = { pass = it },
label = { Text("Password") }
label = { Text(stringResource(R.string.import_keystore_dialog_password_field)) }
)

Text("Credential presets")

Button(
onClick = {
cn = DEFAULT
pass = DEFAULT
}
) {
Text(stringResource(R.string.import_keystore_preset_default))
}
Button(
onClick = {
cn = DEFAULT
pass = FLUTTER_MANAGER_PASSWORD
}
) {
Text(stringResource(R.string.import_keystore_preset_flutter))
}
}
}
)
Expand Down
Loading

0 comments on commit aa02e9f

Please sign in to comment.