Add password config in account detail (#861)

* feat: WIP password configs from AccountDetail

* feat: spotless fixes
This commit is contained in:
Yogesh Choudhary Paliyal
2024-06-02 16:27:31 +05:30
committed by GitHub
parent 59fc381a9d
commit 892f65fc10
18 changed files with 126 additions and 65 deletions

View File

@@ -5,7 +5,6 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.yogeshpaliyal.common.utils.getUserSettings
import com.yogeshpaliyal.common.utils.setDefaultPasswordLength
import com.yogeshpaliyal.keypass.ui.generate.ui.components.DEFAULT_PASSWORD_LENGTH
import com.yogeshpaliyal.keypass.ui.redux.states.ChangeDefaultPasswordLengthState
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -24,7 +23,7 @@ class ChangeDefaultPasswordLengthViewModel : ViewModel() {
viewModelScope.launch {
_viewState.update {
val oldPasswordLength =
context.getUserSettings().defaultPasswordLength ?: DEFAULT_PASSWORD_LENGTH
context.getUserSettings().passwordConfig.length
ChangeDefaultPasswordLengthState(length = oldPasswordLength)
}
}

View File

@@ -17,6 +17,7 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
@@ -29,12 +30,13 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.hilt.navigation.compose.hiltViewModel
import com.yogeshpaliyal.common.constants.ScannerType
import com.yogeshpaliyal.common.data.AccountModel
import com.yogeshpaliyal.common.utils.TOTPHelper
import com.yogeshpaliyal.keypass.ui.detail.components.BottomBar
import com.yogeshpaliyal.keypass.ui.detail.components.Fields
import com.yogeshpaliyal.keypass.ui.redux.actions.CopyToClipboard
import com.yogeshpaliyal.keypass.ui.redux.actions.GoBackAction
import com.yogeshpaliyal.keypass.ui.redux.actions.NavigationAction
import com.yogeshpaliyal.keypass.ui.redux.states.PasswordGeneratorState
import org.reduxkotlin.compose.rememberDispatcher
import java.io.File
import java.io.FileOutputStream
@@ -60,17 +62,11 @@ fun AccountDetailPage(
val showDialog = remember { mutableStateOf(false) }
// task value state
val (accountModel, setAccountModel) = remember {
mutableStateOf(
AccountModel()
)
}
val accountModel = viewModel.accountModel.collectAsState().value
// Set initial object
LaunchedEffect(key1 = id) {
viewModel.loadAccount(id) {
setAccountModel(it.copy())
}
viewModel.loadAccount(id)
}
val goBack: () -> Unit = {
@@ -80,7 +76,7 @@ fun AccountDetailPage(
val launcher = rememberLauncherForActivityResult(QRScanner()) {
when (it.type) {
ScannerType.Password -> {
setAccountModel(accountModel.copy(password = it.scannedText))
viewModel.setAccountModel(accountModel.copy(password = it.scannedText))
}
ScannerType.Secret -> {
it.scannedText ?: return@rememberLauncherForActivityResult
@@ -94,7 +90,7 @@ fun AccountDetailPage(
if (newAccountModel.username.isNullOrEmpty()) {
newAccountModel = newAccountModel.copy(username = totp.issuer)
}
setAccountModel(newAccountModel)
viewModel.setAccountModel(newAccountModel)
}
}
}
@@ -111,6 +107,9 @@ fun AccountDetailPage(
val qrCodeBitmap = viewModel.generateQrCode(accountModel)
generatedQrCodeBitmap.value = qrCodeBitmap
},
openPasswordConfiguration = {
dispatchAction(NavigationAction(PasswordGeneratorState()))
}
) {
viewModel.insertOrUpdate(accountModel, goBack)
@@ -121,7 +120,7 @@ fun AccountDetailPage(
Fields(
accountModel = accountModel,
updateAccountModel = { newAccountModel ->
setAccountModel(newAccountModel)
viewModel.setAccountModel(newAccountModel)
},
copyToClipboardClicked = { value ->
dispatchAction(CopyToClipboard(value))

View File

@@ -3,8 +3,6 @@ package com.yogeshpaliyal.keypass.ui.detail
import android.app.Application
import android.graphics.Bitmap
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.google.gson.Gson
import com.google.zxing.BarcodeFormat
@@ -16,6 +14,8 @@ import com.yogeshpaliyal.common.data.AccountModel
import com.yogeshpaliyal.common.worker.executeAutoBackup
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.*
@@ -33,15 +33,23 @@ class DetailViewModel @Inject constructor(
val appDb: com.yogeshpaliyal.common.AppDatabase
) : AndroidViewModel(app) {
private val _accountModel by lazy { MutableLiveData<AccountModel>() }
val accountModel: LiveData<AccountModel> = _accountModel
private val _accountModel by lazy { MutableStateFlow<AccountModel>(AccountModel()) }
val accountModel: StateFlow<AccountModel> = _accountModel
fun loadAccount(id: Long?, getAccount: (AccountModel) -> Unit) {
fun loadAccount(id: Long?) {
viewModelScope.launch(Dispatchers.IO) {
getAccount(appDb.getDao().getAccount(id) ?: AccountModel())
if (id == null) {
_accountModel.emit(AccountModel())
} else {
_accountModel.emit(appDb.getDao().getAccount(id) ?: AccountModel())
}
}
}
fun setAccountModel(accountModel: AccountModel) {
_accountModel.value = accountModel
}
fun deleteAccount(accountModel: AccountModel, onExecCompleted: () -> Unit) {
viewModelScope.launch {
accountModel.let {

View File

@@ -1,6 +1,7 @@
package com.yogeshpaliyal.keypass.ui.detail.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Password
import androidx.compose.material.icons.filled.QrCode
import androidx.compose.material.icons.rounded.ArrowBackIosNew
import androidx.compose.material.icons.rounded.Delete
@@ -24,6 +25,7 @@ fun BottomBar(
backPressed: () -> Unit,
onDeleteAccount: () -> Unit,
generateQrCodeClicked: () -> Unit,
openPasswordConfiguration: () -> Unit,
onSaveClicked: () -> Unit
) {
val openDialog = remember { mutableStateOf(false) }
@@ -38,6 +40,17 @@ fun BottomBar(
)
}
IconButton(
modifier = Modifier.testTag("action_configure_password"),
onClick = { openPasswordConfiguration() }
) {
Icon(
painter = rememberVectorPainter(image = Icons.Default.Password),
contentDescription = "Open Password Configuration",
tint = MaterialTheme.colorScheme.onSurface
)
}
if (accountModel.id != null) {
IconButton(
modifier = Modifier.testTag("action_delete"),

View File

@@ -30,6 +30,7 @@ import com.yogeshpaliyal.common.utils.PasswordGenerator
import com.yogeshpaliyal.keypass.R
import com.yogeshpaliyal.keypass.ui.commonComponents.KeyPassInputField
import com.yogeshpaliyal.keypass.ui.commonComponents.PasswordTrailingIcon
import com.yogeshpaliyal.keypass.ui.nav.LocalUserSettings
@Composable
fun Fields(
@@ -39,6 +40,8 @@ fun Fields(
copyToClipboardClicked: (String) -> Unit,
scanClicked: (scannerType: Int) -> Unit
) {
val passwordConfig = LocalUserSettings.current.passwordConfig
Column(
modifier = modifier
.fillMaxSize()
@@ -90,7 +93,7 @@ fun Fields(
{
IconButton(
onClick = {
updateAccountModel(accountModel.copy(password = PasswordGenerator().generatePassword()))
updateAccountModel(accountModel.copy(password = PasswordGenerator(passwordConfig).generatePassword()))
}
) {
Icon(

View File

@@ -14,7 +14,6 @@ class GeneratePasswordActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.retrieveSavedPasswordLength(baseContext)
setContent {
Mdc3Theme {
GeneratePasswordScreen(viewModel)

View File

@@ -3,10 +3,10 @@ package com.yogeshpaliyal.keypass.ui.generate
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.yogeshpaliyal.common.data.PasswordConfig
import com.yogeshpaliyal.common.utils.PasswordGenerator
import com.yogeshpaliyal.common.utils.getUserSettings
import com.yogeshpaliyal.common.utils.setDefaultPasswordLength
import com.yogeshpaliyal.keypass.ui.generate.ui.components.DEFAULT_PASSWORD_LENGTH
import com.yogeshpaliyal.common.utils.setPasswordConfig
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.FlowPreview
@@ -23,18 +23,18 @@ class GeneratePasswordViewModel @Inject constructor(
@ApplicationContext context: Context
) : ViewModel() {
private val _viewState = MutableStateFlow(GeneratePasswordViewState.Initial)
private val _viewState = MutableStateFlow(PasswordConfig.Initial)
val viewState = _viewState.asStateFlow()
init {
observeState(context)
}
fun retrieveSavedPasswordLength(context: Context) {
fun retrieveSavedPasswordConfig(context: Context) {
viewModelScope.launch {
val savedPasswordLength = context.getUserSettings().defaultPasswordLength ?: DEFAULT_PASSWORD_LENGTH
val passwordConfig = context.getUserSettings().passwordConfig
_viewState.update {
_viewState.value.copy(length = savedPasswordLength)
passwordConfig
}
}
}
@@ -43,12 +43,7 @@ class GeneratePasswordViewModel @Inject constructor(
val currentViewState = _viewState.value
val passwordGenerator = PasswordGenerator(
length = currentViewState.length.toInt(),
includeUpperCaseLetters = currentViewState.includeUppercaseLetters,
includeLowerCaseLetters = currentViewState.includeLowercaseLetters,
includeSymbols = currentViewState.includeSymbols,
includeNumbers = currentViewState.includeNumbers,
includeBlankSpaces = currentViewState.includeBlankSpaces
currentViewState
)
_viewState.update {
@@ -100,7 +95,7 @@ class GeneratePasswordViewModel @Inject constructor(
_viewState
.debounce(400)
.collectLatest { state ->
context.setDefaultPasswordLength(state.length)
context.setPasswordConfig(state)
}
}
}

View File

@@ -1,7 +1,9 @@
package com.yogeshpaliyal.keypass.ui.generate
import androidx.annotation.Keep
import com.yogeshpaliyal.keypass.ui.generate.ui.components.DEFAULT_PASSWORD_LENGTH
@Keep
data class GeneratePasswordViewState(
val length: Float,
val includeUppercaseLetters: Boolean,

View File

@@ -23,13 +23,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.google.accompanist.themeadapter.material3.Mdc3Theme
import com.yogeshpaliyal.keypass.ui.generate.GeneratePasswordViewState
import com.yogeshpaliyal.common.data.PasswordConfig
import com.yogeshpaliyal.keypass.ui.generate.ui.components.CheckboxWithLabel
import com.yogeshpaliyal.keypass.ui.generate.ui.components.PasswordLengthInput
@Composable
fun GeneratePasswordContent(
viewState: GeneratePasswordViewState,
viewState: PasswordConfig,
onCopyPasswordClick: () -> Unit,
onGeneratePasswordClick: () -> Unit,
onPasswordLengthChange: (Float) -> Unit,
@@ -77,7 +77,7 @@ private fun GeneratePasswordFab(onGeneratePasswordClick: () -> Unit) {
@Composable
private fun FormInputCard(
viewState: GeneratePasswordViewState,
viewState: PasswordConfig,
onCopyPasswordClick: () -> Unit,
onPasswordLengthChange: (Float) -> Unit,
onUppercaseCheckedChange: (Boolean) -> Unit,
@@ -211,7 +211,7 @@ private fun BlankSpaceInput(
@Composable
@Suppress("UnusedPrivateMember", "MagicNumber")
private fun GeneratePasswordContentPreview() {
val viewState = GeneratePasswordViewState.Initial
val viewState = PasswordConfig.Initial
Mdc3Theme {
GeneratePasswordContent(

View File

@@ -4,22 +4,22 @@ import android.content.Context
import android.widget.Toast
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.yogeshpaliyal.keypass.R
import com.yogeshpaliyal.keypass.ui.generate.GeneratePasswordViewModel
import com.yogeshpaliyal.keypass.ui.generate.ui.utils.copyTextToClipboard
@Composable
fun GeneratePasswordScreen(viewModel: GeneratePasswordViewModel) {
fun GeneratePasswordScreen(viewModel: GeneratePasswordViewModel = hiltViewModel()) {
val context = LocalContext.current
// replace collectAsState() with collectAsStateWithLifecycle() when compose version and kotlin version are bumped up.
val viewState by viewModel.viewState.collectAsState()
val viewState by viewModel.viewState.collectAsStateWithLifecycle()
LaunchedEffect(key1 = Unit) {
viewModel.generatePassword()
LaunchedEffect(Unit) {
viewModel.retrieveSavedPasswordConfig(context)
}
GeneratePasswordContent(

View File

@@ -31,6 +31,7 @@ import com.yogeshpaliyal.keypass.ui.backupsImport.BackupImporter
import com.yogeshpaliyal.keypass.ui.changeDefaultPasswordLength.ChangeDefaultPasswordLengthScreen
import com.yogeshpaliyal.keypass.ui.changePassword.ChangePassword
import com.yogeshpaliyal.keypass.ui.detail.AccountDetailPage
import com.yogeshpaliyal.keypass.ui.generate.ui.GeneratePasswordScreen
import com.yogeshpaliyal.keypass.ui.home.Homepage
import com.yogeshpaliyal.keypass.ui.nav.components.DashboardBottomSheet
import com.yogeshpaliyal.keypass.ui.nav.components.KeyPassBottomBar
@@ -46,6 +47,7 @@ import com.yogeshpaliyal.keypass.ui.redux.states.ChangeAppPasswordState
import com.yogeshpaliyal.keypass.ui.redux.states.ChangeDefaultPasswordLengthState
import com.yogeshpaliyal.keypass.ui.redux.states.HomeState
import com.yogeshpaliyal.keypass.ui.redux.states.KeyPassState
import com.yogeshpaliyal.keypass.ui.redux.states.PasswordGeneratorState
import com.yogeshpaliyal.keypass.ui.redux.states.ScreenState
import com.yogeshpaliyal.keypass.ui.redux.states.SettingsState
import com.yogeshpaliyal.keypass.ui.settings.MySettingCompose
@@ -169,6 +171,7 @@ fun CurrentPage() {
is BackupImporterState -> BackupImporter(state = it)
is AboutState -> AboutScreen()
is PasswordGeneratorState -> GeneratePasswordScreen()
}
}
}

View File

@@ -0,0 +1,3 @@
package com.yogeshpaliyal.keypass.ui.redux.states
class PasswordGeneratorState : ScreenState(false)

View File

@@ -64,7 +64,7 @@ fun MySettingCompose() {
// Retrieving saved password length
var savedPasswordLength by remember { mutableStateOf(DEFAULT_PASSWORD_LENGTH) }
LaunchedEffect(key1 = Unit) {
userSettings.defaultPasswordLength.let { value -> savedPasswordLength = value }
userSettings.passwordConfig.length.let { value -> savedPasswordLength = value }
}
Column(modifier = Modifier.fillMaxSize(1f).verticalScroll(rememberScrollState())) {

View File

@@ -28,7 +28,7 @@ class SharedPreferenceUtilsTest {
@Test
fun getKeyPassPasswordLength_test() = runBlocking {
val result = context.getUserSettings().defaultPasswordLength
val result = context.getUserSettings().passwordConfig.length
assertEquals(DEFAULT_PASSWORD_LENGTH, result)
}
@@ -36,7 +36,7 @@ class SharedPreferenceUtilsTest {
fun setKeyPassPasswordLength_test() = runBlocking {
val expectedLength = 8f
context.setDefaultPasswordLength(expectedLength)
val result = context.getUserSettings().defaultPasswordLength
val result = context.getUserSettings().passwordConfig.length
assertEquals(expectedLength, result)
}

View File

@@ -0,0 +1,28 @@
package com.yogeshpaliyal.common.data
import androidx.annotation.Keep
import kotlinx.serialization.Serializable
@Keep
@Serializable
data class PasswordConfig(
val length: Float,
val includeUppercaseLetters: Boolean,
val includeLowercaseLetters: Boolean,
val includeSymbols: Boolean,
val includeNumbers: Boolean,
val includeBlankSpaces: Boolean,
val password: String
) {
companion object {
val Initial = PasswordConfig(
length = DEFAULT_PASSWORD_LENGTH,
includeUppercaseLetters = true,
includeLowercaseLetters = true,
includeSymbols = true,
includeNumbers = true,
includeBlankSpaces = true,
password = ""
)
}
}

View File

@@ -10,6 +10,7 @@ const val DEFAULT_PASSWORD_LENGTH = 10f
data class UserSettings(
val keyPassPassword: String? = null,
val dbPassword: String? = null,
@Deprecated("Use passwordConfig instead")
val defaultPasswordLength: Float = DEFAULT_PASSWORD_LENGTH,
val backupKey: String? = null,
val isBiometricEnable: Boolean = false,
@@ -18,7 +19,8 @@ data class UserSettings(
val autoBackupEnable: Boolean = false,
val overrideAutoBackup: Boolean = false,
val lastAppVersion: Int? = null,
val currentAppVersion: Int? = null
val currentAppVersion: Int? = null,
val passwordConfig: PasswordConfig = PasswordConfig.Initial
) {
fun isKeyPresent() = backupKey != null
}

View File

@@ -1,15 +1,11 @@
package com.yogeshpaliyal.common.utils
class PasswordGenerator(
private var length: Int,
private var includeUpperCaseLetters: Boolean,
private var includeLowerCaseLetters: Boolean,
private var includeSymbols: Boolean,
private var includeNumbers: Boolean,
private var includeBlankSpaces: Boolean
) {
import com.yogeshpaliyal.common.data.PasswordConfig
constructor() : this(10, true, true, true, true, true) private val UPPER_CASE = 0
class PasswordGenerator(
val passwordConfig: PasswordConfig
) {
private val UPPER_CASE = 0
private val LOWER_CASE = 1
private val NUMBERS = 2
private val SYMBOLS = 3
@@ -18,24 +14,24 @@ class PasswordGenerator(
fun generatePassword(): String {
var password = ""
val list = ArrayList<Int>()
if (includeUpperCaseLetters) {
if (passwordConfig.includeUppercaseLetters) {
list.add(UPPER_CASE)
}
if (includeLowerCaseLetters) {
if (passwordConfig.includeLowercaseLetters) {
list.add(LOWER_CASE)
}
if (includeNumbers) {
if (passwordConfig.includeNumbers) {
list.add(NUMBERS)
}
if (includeSymbols) {
if (passwordConfig.includeSymbols) {
list.add(SYMBOLS)
}
if (includeBlankSpaces) {
if (passwordConfig.includeBlankSpaces) {
list.add(BLANKSPACES)
}
for (i in 1..length) {
for (i in 1..passwordConfig.length.toInt()) {
if (list.isNotEmpty()) {
when (list.random()) {
UPPER_CASE -> password += ('A'..'Z').random().toString()

View File

@@ -9,6 +9,7 @@ import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import com.yogeshpaliyal.common.data.DEFAULT_PASSWORD_LENGTH
import com.yogeshpaliyal.common.data.PasswordConfig
import com.yogeshpaliyal.common.data.UserSettings
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
@@ -32,6 +33,12 @@ private fun Context.getUserSettingsDataStore(): DataStore<UserSettings> {
return res
}
suspend fun Context.setPasswordConfig(passwordConfig: PasswordConfig) {
getUserSettingsDataStore().updateData {
it.copy(passwordConfig = passwordConfig)
}
}
suspend fun Context.getUserSettings(): UserSettings {
return getUserSettingsDataStore().data.firstOrNull() ?: UserSettings()
}
@@ -52,7 +59,7 @@ suspend fun Context.setKeyPassPassword(password: String?) {
suspend fun Context.setDefaultPasswordLength(password: Float) {
getUserSettingsDataStore().updateData {
it.copy(defaultPasswordLength = password)
it.copy(passwordConfig = it.passwordConfig.copy(length = password))
}
}
@@ -159,6 +166,10 @@ suspend fun Context.migrateOldDataToNewerDataStore() {
userSettings = userSettings.copy(defaultPasswordLength = olderData[KEYPASS_PASSWORD_LENGTH] ?: DEFAULT_PASSWORD_LENGTH)
}
if (userSettings.defaultPasswordLength != DEFAULT_PASSWORD_LENGTH) {
userSettings = userSettings.copy(passwordConfig = userSettings.passwordConfig.copy(length = userSettings.defaultPasswordLength))
}
if (olderData.contains(BACKUP_DIRECTORY)) {
userSettings = userSettings.copy(backupDirectory = olderData[BACKUP_DIRECTORY])
}