diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/EncryptRecipientScreen.kt b/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/EncryptRecipientScreen.kt index fd0cf6cb8..f7ca040c2 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/EncryptRecipientScreen.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/EncryptRecipientScreen.kt @@ -26,6 +26,7 @@ import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -50,6 +51,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -78,7 +80,10 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController import ee.ria.DigiDoc.R import ee.ria.DigiDoc.cryptolib.Addressee +import ee.ria.DigiDoc.domain.model.settings.CDOCSetting +import ee.ria.DigiDoc.ui.component.crypto.EncryptPasswordDialog import ee.ria.DigiDoc.ui.component.crypto.bottombar.EncryptBottomBar +import ee.ria.DigiDoc.ui.component.crypto.bottombar.EncryptButtonBottomBar import ee.ria.DigiDoc.ui.component.crypto.bottomsheet.RecipientBottomSheet import ee.ria.DigiDoc.ui.component.menu.SettingsMenuBottomSheet import ee.ria.DigiDoc.ui.component.shared.InvisibleElement @@ -86,6 +91,7 @@ import ee.ria.DigiDoc.ui.component.shared.LoadingScreen import ee.ria.DigiDoc.ui.component.shared.MessageDialog import ee.ria.DigiDoc.ui.component.shared.PreventResize import ee.ria.DigiDoc.ui.component.shared.Recipient +import ee.ria.DigiDoc.ui.component.shared.TabView import ee.ria.DigiDoc.ui.component.shared.TopBar import ee.ria.DigiDoc.ui.theme.Dimensions.SPadding import ee.ria.DigiDoc.ui.theme.Dimensions.XSPadding @@ -102,6 +108,7 @@ import ee.ria.DigiDoc.utils.snackbar.SnackBarManager import ee.ria.DigiDoc.utils.snackbar.SnackBarManager.showMessage import ee.ria.DigiDoc.utilsLib.validator.PersonalCodeValidator import ee.ria.DigiDoc.viewmodel.EncryptRecipientViewModel +import ee.ria.DigiDoc.viewmodel.EncryptViewModel import ee.ria.DigiDoc.viewmodel.shared.SharedContainerViewModel import ee.ria.DigiDoc.viewmodel.shared.SharedMenuViewModel import ee.ria.DigiDoc.viewmodel.shared.SharedRecipientViewModel @@ -125,8 +132,6 @@ fun EncryptRecipientScreen( val scope = rememberCoroutineScope() - val focusManager = LocalFocusManager.current - val snackBarHostState = remember { SnackbarHostState() } val snackBarScope = rememberCoroutineScope() @@ -135,6 +140,7 @@ fun EncryptRecipientScreen( val showLoading = remember { mutableStateOf(false) } val isSettingsMenuBottomSheetVisible = rememberSaveable { mutableStateOf(false) } + val showPasswordDialog = rememberSaveable { mutableStateOf(false) } val recipientAddedSuccess = remember { mutableStateOf(false) } val recipientAddedSuccessText = stringResource(id = R.string.crypto_recipients_recipient_add_success) @@ -175,7 +181,19 @@ fun EncryptRecipientScreen( sendAccessibilityEvent(context, getAccessibilityEventType(), recipientRemovalCancelled) } - val listState = rememberLazyListState() + val encryptViewModel: EncryptViewModel = + hiltViewModel( + viewModelStoreOwner = + remember { + navController.getBackStackEntry(Route.Encrypt.route) + }, + ) + val isCdoc2 = encryptViewModel.cdocSetting == CDOCSetting.CDOC2 + var selectedTabIndex by rememberSaveable { mutableIntStateOf(0) } + + val tabRecipientTitle = stringResource(R.string.crypto_encrypt_tab_recipient) + val tabPasswordTitle = stringResource(R.string.crypto_encrypt_tab_password) + var expanded by rememberSaveable { mutableStateOf(false) } val searchText by encryptRecipientViewModel.searchText.collectAsState() val recipientList by encryptRecipientViewModel.recipientList.collectAsState() @@ -294,20 +312,31 @@ fun EncryptRecipientScreen( }, bottomBar = { if (cryptoContainer != null) { - EncryptBottomBar( - modifier = modifier, - isEncryptButtonEnabled = encryptionButtonEnabled.value, - onEncryptClick = { - if (encryptionButtonEnabled.value) { - encryptionButtonEnabled.value = false - showLoading.value = true - scope.launch(Main) { - encryptRecipientViewModel.encryptContainer(sharedContainerViewModel) - showLoading.value = false + if (isCdoc2 && selectedTabIndex == 1) { + EncryptButtonBottomBar( + modifier = modifier, + encryptButtonIcon = R.drawable.ic_m3_arrow_forward_48dp_wght400, + encryptButtonName = R.string.next_button, + encryptButtonContentDescription = R.string.next_button, + isEncryptButtonEnabled = true, + onEncryptButtonClick = { showPasswordDialog.value = true }, + ) + } else { + EncryptBottomBar( + modifier = modifier, + isEncryptButtonEnabled = encryptionButtonEnabled.value, + onEncryptClick = { + if (encryptionButtonEnabled.value) { + encryptionButtonEnabled.value = false + showLoading.value = true + scope.launch(Main) { + encryptRecipientViewModel.encryptContainer(sharedContainerViewModel) + showLoading.value = false + } } - } - }, - ) + }, + ) + } } }, ) { paddingValues -> @@ -316,268 +345,83 @@ fun EncryptRecipientScreen( isBottomSheetVisible = isSettingsMenuBottomSheetVisible, ) - Column( - modifier = - modifier - .padding(paddingValues) - .fillMaxWidth(), - horizontalAlignment = Alignment.Start, - ) { - if (!expanded) { - Text( - text = stringResource(id = R.string.crypto_container_recipients_title), - maxLines = 2, - modifier = - modifier - .fillMaxWidth() - .padding(SPadding) - .semantics { heading() } - .focusable(enabled = true) - .focusTarget() - .focusProperties { canFocus = true }, - textAlign = TextAlign.Start, - style = MaterialTheme.typography.headlineSmall, - ) - } - val searchBarPadding = - if (!expanded) { - SPadding - } else { - zeroPadding - } - SearchBar( - modifier = - modifier - .padding(horizontal = searchBarPadding), - inputField = { - SearchBarDefaults.InputField( - modifier = - modifier - .fillMaxWidth() - .wrapContentHeight(), - query = searchText, - onQueryChange = encryptRecipientViewModel::onSearchTextChange, - onSearch = { - if (searchText.isDigitsOnly() && - searchText.length == 11 && - !PersonalCodeValidator.isPersonalCodeValid(searchText) - ) { - showMessage(invalidPersonalCodeMessage) - return@InputField - } - encryptRecipientViewModel.onQueryTextChange(searchText) - focusManager.clearFocus() - }, - expanded = expanded, - enabled = true, - placeholder = { - PreventResize { - Text(stringResource(id = R.string.crypto_recipients_search)) - } - }, - leadingIcon = { - Icon( - modifier = - modifier - .size(iconSizeXXS), - imageVector = ImageVector.vectorResource(R.drawable.ic_m3_search_48dp_wght400), - contentDescription = null, - ) - }, - trailingIcon = { - if (expanded) { - IconButton( - modifier = - modifier - .padding(end = XSPadding) - .size(iconSizeXXS) - .testTag("searchCancelButton"), - onClick = dismissSearch, - content = { - Icon( - imageVector = - ImageVector.vectorResource( - R.drawable.ic_m3_close_48dp_wght400, - ), - contentDescription = - stringResource( - id = R.string.crypto_recipients_search_cancel, - ), + if (isCdoc2) { + Column( + modifier = modifier.padding(paddingValues).fillMaxSize(), + ) { + TabView( + modifier = modifier, + testTag = "encryptRecipientTabView", + selectedTabIndex = selectedTabIndex, + onTabSelected = { index -> + selectedTabIndex = index + if (index != 0) expanded = false + }, + tabItems = + listOf( + Pair(tabRecipientTitle) { + RecipientTabContent( + modifier = Modifier.fillMaxSize(), + expanded = expanded, + onExpandedChange = { expanded = it }, + searchText = searchText, + onSearchTextChange = encryptRecipientViewModel::onSearchTextChange, + invalidPersonalCodeMessage = invalidPersonalCodeMessage, + onSearch = { encryptRecipientViewModel.onQueryTextChange(it) }, + onDismissSearch = dismissSearch, + recipientList = recipientList, + hasSearched = hasSearched, + containerRecipientList = containerRecipientList.value, + onAddRecipientToContainer = { recipient -> + encryptRecipientViewModel.addRecipientToContainer( + recipient, + sharedContainerViewModel, ) }, + onRecipientClick = { recipient -> + clickedRecipient.value = recipient + showRecipientBottomSheet.value = true + }, ) - } - }, - onExpandedChange = { expanded = it }, - colors = inputFieldColors(), - interactionSource = null, - ) - }, + }, + Pair(tabPasswordTitle) { + PasswordTabContent(modifier = Modifier.fillMaxSize()) + }, + ), + ) + } + } else { + RecipientTabContent( + modifier = Modifier.padding(paddingValues).fillMaxWidth(), expanded = expanded, onExpandedChange = { expanded = it }, - ) { - LazyColumn( - state = listState, - modifier = modifier.testTag("lazyColumnScrollView"), - ) { - if (recipientList.isNotEmpty()) { - item { - HorizontalDivider( - modifier = - modifier - .fillMaxWidth() - .padding(SPadding) - .height(dividerHeight), - ) - } - items(recipientList) { recipient -> - Recipient( - recipient = recipient, - isMoreOptionsButtonShown = false, - onClick = { - encryptRecipientViewModel.addRecipientToContainer( - recipient, - sharedContainerViewModel, - ) - }, - ) - HorizontalDivider( - modifier = - modifier - .fillMaxWidth() - .padding(SPadding) - .height(dividerHeight), - ) - } - } else if (hasSearched) { - item { - Box( - modifier = - modifier - .fillParentMaxSize() - .padding(SPadding), - contentAlignment = Alignment.Center, - ) { - Text( - modifier = - modifier - .testTag("encryptRecipientsListEmpty"), - textAlign = TextAlign.Center, - text = stringResource(id = R.string.crypto_recipients_search_empty), - color = MaterialTheme.colorScheme.onSurface, - style = MaterialTheme.typography.bodyLarge, - ) - } - } - } - if (containerRecipientList.value.isNotEmpty()) { - item { - Text( - modifier = - modifier - .padding(horizontal = SPadding) - .padding(top = SPadding) - .semantics { - heading() - testTagsAsResourceId = true - }.testTag("encryptRecentlyAddedRecipientsListTitle"), - text = stringResource(R.string.crypto_container_latest_recipients_title), - style = MaterialTheme.typography.bodyMedium, - textAlign = TextAlign.Start, - ) - } - items(containerRecipientList.value) { recipient -> - Recipient( - recipient = recipient, - isMoreOptionsButtonShown = true, - onClick = { - clickedRecipient.value = recipient - showRecipientBottomSheet.value = true - }, - ) - HorizontalDivider( - modifier = - modifier - .fillMaxWidth() - .padding(SPadding) - .height(dividerHeight), - ) - } - } - - item { - Spacer( - modifier = modifier.height(invisibleElementHeight), - ) - if (listState.reachedBottom()) { - InvisibleElement(modifier = modifier) - } - } - } - } - if (!expanded) { - LazyColumn( - state = listState, - modifier = modifier.testTag("lazyColumnScrollView"), - ) { - item { - Text( - modifier = - modifier - .padding(horizontal = SPadding) - .padding(top = SPadding) - .semantics { - heading() - testTagsAsResourceId = true - }.testTag("encryptRecipientsDescription"), - text = stringResource(R.string.crypto_recipients_description), - textAlign = TextAlign.Start, - ) - } - if (containerRecipientList.value.isNotEmpty()) { - item { - Text( - modifier = - modifier - .padding(horizontal = SPadding) - .padding(top = SPadding) - .semantics { - heading() - testTagsAsResourceId = true - }.testTag("encryptRecipientsListTitle"), - text = stringResource(R.string.crypto_container_added_recipients_title), - style = MaterialTheme.typography.bodyMedium, - textAlign = TextAlign.Start, - ) - } - items(containerRecipientList.value) { recipient -> - Recipient( - recipient = recipient, - onClick = { - clickedRecipient.value = recipient - showRecipientBottomSheet.value = true - }, - ) - HorizontalDivider( - modifier = - modifier - .fillMaxWidth() - .padding(SPadding) - .height(dividerHeight), - ) - } + searchText = searchText, + onSearchTextChange = encryptRecipientViewModel::onSearchTextChange, + invalidPersonalCodeMessage = invalidPersonalCodeMessage, + onSearch = { encryptRecipientViewModel.onQueryTextChange(it) }, + onDismissSearch = dismissSearch, + recipientList = recipientList, + hasSearched = hasSearched, + containerRecipientList = containerRecipientList.value, + onAddRecipientToContainer = { recipient -> + encryptRecipientViewModel.addRecipientToContainer( + recipient, + sharedContainerViewModel, + ) + }, + onRecipientClick = { recipient -> + clickedRecipient.value = recipient + showRecipientBottomSheet.value = true + }, + ) + } - item { - Spacer( - modifier = modifier.height(invisibleElementHeight), - ) - if (listState.reachedBottom()) { - InvisibleElement(modifier = modifier) - } - } - } - } - } + if (showPasswordDialog.value) { + EncryptPasswordDialog( + modifier = modifier, + onDismiss = { showPasswordDialog.value = false }, + onEncrypt = { _, _ -> showPasswordDialog.value = false }, + ) } if (openRemoveRecipientDialog.value) { @@ -607,7 +451,7 @@ fun EncryptRecipientScreen( ) } - if (showLoading.value == true) { + if (showLoading.value) { LoadingScreen(modifier = modifier) } @@ -624,6 +468,273 @@ fun EncryptRecipientScreen( } } +@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class) +@Composable +private fun RecipientTabContent( + modifier: Modifier = Modifier, + expanded: Boolean, + onExpandedChange: (Boolean) -> Unit, + searchText: String, + onSearchTextChange: (String) -> Unit, + invalidPersonalCodeMessage: String, + onSearch: (String) -> Unit, + onDismissSearch: () -> Unit, + recipientList: List, + hasSearched: Boolean, + containerRecipientList: List, + onAddRecipientToContainer: (Addressee) -> Unit, + onRecipientClick: (Addressee) -> Unit, +) { + val focusManager = LocalFocusManager.current + val searchListState = rememberLazyListState() + val mainListState = rememberLazyListState() + + Column( + modifier = modifier, + horizontalAlignment = Alignment.Start, + ) { + if (!expanded) { + Text( + text = stringResource(id = R.string.crypto_container_recipients_title), + maxLines = 2, + modifier = + Modifier + .fillMaxWidth() + .padding(SPadding) + .semantics { heading() } + .focusable(enabled = true) + .focusTarget() + .focusProperties { canFocus = true }, + textAlign = TextAlign.Start, + style = MaterialTheme.typography.headlineSmall, + ) + } + val searchBarPadding = if (!expanded) SPadding else zeroPadding + SearchBar( + modifier = Modifier.padding(horizontal = searchBarPadding), + inputField = { + SearchBarDefaults.InputField( + modifier = Modifier.fillMaxWidth().wrapContentHeight(), + query = searchText, + onQueryChange = onSearchTextChange, + onSearch = { + if (searchText.isDigitsOnly() && + searchText.length == 11 && + !PersonalCodeValidator.isPersonalCodeValid(searchText) + ) { + showMessage(invalidPersonalCodeMessage) + return@InputField + } + onSearch(searchText) + focusManager.clearFocus() + }, + expanded = expanded, + enabled = true, + placeholder = { + PreventResize { + Text(stringResource(id = R.string.crypto_recipients_search)) + } + }, + leadingIcon = { + Icon( + modifier = Modifier.size(iconSizeXXS), + imageVector = ImageVector.vectorResource(R.drawable.ic_m3_search_48dp_wght400), + contentDescription = null, + ) + }, + trailingIcon = { + if (expanded) { + IconButton( + modifier = + Modifier + .padding(end = XSPadding) + .size(iconSizeXXS) + .testTag("searchCancelButton"), + onClick = onDismissSearch, + content = { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_m3_close_48dp_wght400), + contentDescription = + stringResource( + id = R.string.crypto_recipients_search_cancel, + ), + ) + }, + ) + } + }, + onExpandedChange = onExpandedChange, + colors = inputFieldColors(), + interactionSource = null, + ) + }, + expanded = expanded, + onExpandedChange = onExpandedChange, + ) { + LazyColumn( + state = searchListState, + modifier = Modifier.testTag("lazyColumnScrollView"), + ) { + if (recipientList.isNotEmpty()) { + item { + HorizontalDivider( + modifier = + Modifier + .fillMaxWidth() + .padding(SPadding) + .height(dividerHeight), + ) + } + items(recipientList) { recipient -> + RecipientItem( + recipient = recipient, + isMoreOptionsButtonShown = false, + onClick = { onAddRecipientToContainer(it) }, + ) + } + } else if (hasSearched) { + item { + Box( + modifier = + Modifier + .fillParentMaxSize() + .padding(SPadding), + contentAlignment = Alignment.Center, + ) { + Text( + modifier = Modifier.testTag("encryptRecipientsListEmpty"), + textAlign = TextAlign.Center, + text = stringResource(id = R.string.crypto_recipients_search_empty), + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.bodyLarge, + ) + } + } + } + if (containerRecipientList.isNotEmpty()) { + item { + Text( + modifier = + Modifier + .padding(horizontal = SPadding) + .padding(top = SPadding) + .semantics { + heading() + testTagsAsResourceId = true + }.testTag("encryptRecentlyAddedRecipientsListTitle"), + text = stringResource(R.string.crypto_container_latest_recipients_title), + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Start, + ) + } + items(containerRecipientList) { recipient -> + RecipientItem( + recipient = recipient, + onClick = { onRecipientClick(it) }, + ) + } + } + item { + Spacer(modifier = Modifier.height(invisibleElementHeight)) + if (searchListState.reachedBottom()) { + InvisibleElement(modifier = Modifier) + } + } + } + } + if (!expanded) { + LazyColumn( + state = mainListState, + modifier = Modifier.testTag("lazyColumnScrollView"), + ) { + item { + Text( + modifier = + Modifier + .padding(horizontal = SPadding) + .padding(top = SPadding) + .semantics { + heading() + testTagsAsResourceId = true + }.testTag("encryptRecipientsDescription"), + text = stringResource(R.string.crypto_recipients_description), + textAlign = TextAlign.Start, + ) + } + if (containerRecipientList.isNotEmpty()) { + item { + Text( + modifier = + Modifier + .padding(horizontal = SPadding) + .padding(top = SPadding) + .semantics { + heading() + testTagsAsResourceId = true + }.testTag("encryptRecipientsListTitle"), + text = stringResource(R.string.crypto_container_added_recipients_title), + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Start, + ) + } + items(containerRecipientList) { recipient -> + RecipientItem( + recipient = recipient, + onClick = { onRecipientClick(it) }, + ) + } + item { + Spacer(modifier = Modifier.height(invisibleElementHeight)) + if (mainListState.reachedBottom()) { + InvisibleElement(modifier = Modifier) + } + } + } + } + } + } +} + +@Composable +private fun PasswordTabContent(modifier: Modifier = Modifier) { + LazyColumn(modifier = modifier) { + item { + Text( + modifier = + Modifier + .padding(horizontal = SPadding) + .padding(top = SPadding) + .semantics { + heading() + testTagsAsResourceId = true + }.testTag("encryptPasswordDescription"), + text = stringResource(R.string.crypto_password_encryption_description), + textAlign = TextAlign.Start, + ) + } + } +} + +@Composable +private fun RecipientItem( + recipient: Addressee, + isMoreOptionsButtonShown: Boolean = true, + onClick: (Addressee) -> Unit = {}, +) { + Recipient( + recipient = recipient, + isMoreOptionsButtonShown = isMoreOptionsButtonShown, + onClick = onClick, + ) + HorizontalDivider( + modifier = + Modifier + .fillMaxWidth() + .padding(SPadding) + .height(dividerHeight), + ) +} + @Preview(showBackground = true) @Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/crypto/DecryptPasswordDialog.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/crypto/DecryptPasswordDialog.kt new file mode 100644 index 000000000..f67c5602d --- /dev/null +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/crypto/DecryptPasswordDialog.kt @@ -0,0 +1,172 @@ +/* + * Copyright 2017 - 2026 Riigi Infosüsteemi Amet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +@file:Suppress("PackageName", "FunctionName") + +package ee.ria.DigiDoc.ui.component.crypto + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +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.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.heading +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.window.Dialog +import ee.ria.DigiDoc.R +import ee.ria.DigiDoc.ui.component.shared.CancelAndOkButtonRow +import ee.ria.DigiDoc.ui.component.shared.PrimaryTextField +import ee.ria.DigiDoc.ui.theme.Dimensions.MPadding +import ee.ria.DigiDoc.ui.theme.RIADigiDocTheme +import ee.ria.DigiDoc.utils.extensions.notAccessible + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun DecryptPasswordDialog( + modifier: Modifier = Modifier, + keyLabel: String, + onDismiss: () -> Unit = {}, + onDecrypt: (password: String) -> Unit = {}, +) { + var password by rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf(TextFieldValue("")) + } + + val containerKeyLabelTitle = stringResource(R.string.crypto_password_key_label_placeholder) + val containerPasswordLabel = stringResource(R.string.crypto_password_enter) + + Dialog(onDismissRequest = onDismiss) { + Surface( + modifier = + modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()), + shape = MaterialTheme.shapes.extraLarge, + color = MaterialTheme.colorScheme.surface, + ) { + Column( + modifier = + modifier + .padding(MPadding) + .fillMaxWidth(), + horizontalAlignment = Alignment.Start, + ) { + Text( + text = stringResource(R.string.decrypt_button), + modifier = + modifier + .fillMaxWidth() + .semantics { heading() }, + style = MaterialTheme.typography.titleLarge, + textAlign = TextAlign.Start, + ) + + Spacer(modifier = modifier.height(MPadding)) + + Text( + text = containerKeyLabelTitle, + modifier = + modifier + .fillMaxWidth() + .notAccessible(), + textAlign = TextAlign.Start, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Text( + text = keyLabel, + modifier = + modifier + .fillMaxWidth() + .semantics { + contentDescription = "$containerKeyLabelTitle, $keyLabel" + }, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Start, + ) + + Spacer(modifier = modifier.height(MPadding)) + + PrimaryTextField( + modifier = modifier.fillMaxWidth(), + value = password, + onValueChange = { password = it }, + label = containerPasswordLabel, + placeholder = containerPasswordLabel, + isPasswordText = true, + keyboardOptions = + KeyboardOptions( + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Done, + ), + ) + + Spacer(modifier = modifier.height(MPadding)) + + CancelAndOkButtonRow( + modifier = modifier, + cancelButtonClick = onDismiss, + okButtonClick = { onDecrypt(password.text) }, + okButtonEnabled = password.text.isNotEmpty(), + cancelButtonTitle = R.string.cancel_button, + okButtonTitle = R.string.decrypt_button, + cancelButtonContentDescription = + stringResource(R.string.cancel_button).lowercase(), + okButtonContentDescription = + stringResource(R.string.decrypt_button).lowercase(), + cancelButtonTestTag = "decryptPasswordDialogCancelButton", + okButtonTestTag = "decryptPasswordDialogDecryptButton", + ) + } + } + } +} + +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +fun DecryptPasswordDialogPreview() { + RIADigiDocTheme { + DecryptPasswordDialog(keyLabel = "Allkirjastamata lepingud 2026") + } +} diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/crypto/EncryptNavigation.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/crypto/EncryptNavigation.kt index 9af133278..70bbc0e00 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/crypto/EncryptNavigation.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/crypto/EncryptNavigation.kt @@ -87,6 +87,7 @@ import androidx.navigation.compose.rememberNavController import ee.ria.DigiDoc.R import ee.ria.DigiDoc.common.Constant.DDOC_MIMETYPE import ee.ria.DigiDoc.cryptolib.Addressee +import ee.ria.DigiDoc.cryptolib.CertType import ee.ria.DigiDoc.cryptolib.CryptoContainer import ee.ria.DigiDoc.libdigidoclib.SignedContainer import ee.ria.DigiDoc.ui.component.crypto.bottombar.CryptoNextBottomBar @@ -266,6 +267,8 @@ fun EncryptNavigation( val listState = rememberLazyListState() val showContainerCloseConfirmationDialog = rememberSaveable { mutableStateOf(false) } + val showDecryptPasswordDialog = rememberSaveable { mutableStateOf(false) } + var decryptPasswordKeyLabel by rememberSaveable { mutableStateOf("") } val showContainerBottomSheet = remember { mutableStateOf(false) } val showDataFileBottomSheet = remember { mutableStateOf(false) } @@ -795,9 +798,18 @@ fun EncryptNavigation( onLeftActionButtonClick = onSignActionClick, onRightActionButtonClick = { if (encryptViewModel.isDecryptButtonShown(cryptoContainer, isNestedContainer)) { - showLoadingScreen.value = true - navController.navigate(Route.DecryptScreen.route) - showLoadingScreen.value = false + val passwordRecipient = + cryptoContainer + ?.recipients + ?.firstOrNull { it.certType == CertType.PasswordType } + if (passwordRecipient != null) { + decryptPasswordKeyLabel = passwordRecipient.identifier + showDecryptPasswordDialog.value = true + } else { + showLoadingScreen.value = true + navController.navigate(Route.DecryptScreen.route) + showLoadingScreen.value = false + } } else if (encryptViewModel.isEncryptButtonShown( cryptoContainer, isNestedContainer, @@ -1067,6 +1079,15 @@ fun EncryptNavigation( } } + if (showDecryptPasswordDialog.value) { + DecryptPasswordDialog( + modifier = modifier, + keyLabel = decryptPasswordKeyLabel, + onDismiss = { showDecryptPasswordDialog.value = false }, + onDecrypt = { _ -> showDecryptPasswordDialog.value = false }, + ) + } + SivaConfirmationDialog( showDialog = showSivaDialog, modifier = modifier, diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/crypto/EncryptPasswordDialog.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/crypto/EncryptPasswordDialog.kt new file mode 100644 index 000000000..449b64ecb --- /dev/null +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/crypto/EncryptPasswordDialog.kt @@ -0,0 +1,265 @@ +/* + * Copyright 2017 - 2026 Riigi Infosüsteemi Amet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +@file:Suppress("PackageName", "FunctionName") + +package ee.ria.DigiDoc.ui.component.crypto + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +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.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.heading +import androidx.compose.ui.semantics.isTraversalGroup +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.traversalIndex +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.window.Dialog +import ee.ria.DigiDoc.R +import ee.ria.DigiDoc.ui.component.shared.CancelAndOkButtonRow +import ee.ria.DigiDoc.ui.component.shared.PrimaryTextField +import ee.ria.DigiDoc.ui.theme.Dimensions.MPadding +import ee.ria.DigiDoc.ui.theme.Dimensions.SPadding +import ee.ria.DigiDoc.ui.theme.Dimensions.XSPadding +import ee.ria.DigiDoc.ui.theme.Dimensions.iconSizeXXS +import ee.ria.DigiDoc.ui.theme.RIADigiDocTheme + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun EncryptPasswordDialog( + modifier: Modifier = Modifier, + onDismiss: () -> Unit = {}, + onEncrypt: (keyLabel: String, password: String) -> Unit = { _, _ -> }, +) { + var keyLabel by rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf(TextFieldValue("")) + } + var password by rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf(TextFieldValue("")) + } + var repeatPassword by rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf(TextFieldValue("")) + } + + val keyLabelLabel = stringResource(R.string.crypto_password_key_label_placeholder) + val passwordLabel = stringResource(R.string.crypto_password_enter) + val repeatPasswordLabel = stringResource(R.string.crypto_password_repeat) + + val requirementsTitle = stringResource(R.string.crypto_password_requirements_title) + val requirementLength = stringResource(R.string.crypto_password_requirement_length) + val requirementNumber = stringResource(R.string.crypto_password_requirement_number) + val requirementUppercase = stringResource(R.string.crypto_password_requirement_uppercase) + val requirementLowercase = stringResource(R.string.crypto_password_requirement_lowercase) + val requirementStrings = listOf(requirementLength, requirementNumber, requirementUppercase, requirementLowercase) + val requirementsContentDescription = "$requirementsTitle: ${listOf( + stringResource(R.string.crypto_password_requirement_length_tts), + stringResource(R.string.crypto_password_requirement_number_tts), + requirementUppercase, + requirementLowercase, + ).joinToString(", ")}" + + Dialog(onDismissRequest = onDismiss) { + Surface( + modifier = + modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()), + shape = MaterialTheme.shapes.extraLarge, + color = MaterialTheme.colorScheme.surface, + ) { + Column( + modifier = + modifier + .padding(MPadding) + .fillMaxWidth(), + horizontalAlignment = Alignment.Start, + ) { + Text( + text = stringResource(R.string.crypto_encrypt_tab_password), + modifier = + modifier + .fillMaxWidth() + .semantics { heading() }, + style = MaterialTheme.typography.titleLarge, + textAlign = TextAlign.Start, + ) + + Spacer(modifier = modifier.height(MPadding)) + + PrimaryTextField( + modifier = modifier.fillMaxWidth(), + value = keyLabel, + onValueChange = { keyLabel = it }, + label = keyLabelLabel, + placeholder = keyLabelLabel, + description = stringResource(R.string.crypto_password_key_label_hint), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), + ) + + Spacer(modifier = modifier.height(MPadding)) + + Surface( + modifier = modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.surfaceVariant, + shape = MaterialTheme.shapes.small, + ) { + Row( + modifier = modifier.padding(SPadding), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_m3_info_48dp_wght400), + contentDescription = null, + modifier = modifier.size(iconSizeXXS), + tint = MaterialTheme.colorScheme.primary, + ) + Spacer(modifier = modifier.width(XSPadding)) + Text( + text = stringResource(R.string.crypto_password_secure_place_info), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, + ) + } + } + + Spacer(modifier = modifier.height(MPadding)) + + Column( + modifier = + modifier + .fillMaxWidth() + .semantics { isTraversalGroup = true }, + ) { + PrimaryTextField( + modifier = + modifier + .fillMaxWidth() + .semantics { + isTraversalGroup = true + traversalIndex = 1f + }, + value = password, + onValueChange = { password = it }, + label = passwordLabel, + placeholder = passwordLabel, + isPasswordText = true, + keyboardOptions = + KeyboardOptions( + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Next, + ), + ) + + Spacer(modifier = modifier.height(SPadding)) + + Column( + modifier = + modifier + .fillMaxWidth() + .clearAndSetSemantics { + traversalIndex = 0f + contentDescription = requirementsContentDescription + }, + ) { + requirementStrings.forEach { req -> + Text( + text = "• $req", + modifier = modifier.fillMaxWidth(), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSecondaryContainer, + textAlign = TextAlign.Start, + ) + } + } + } + + Spacer(modifier = modifier.height(MPadding)) + + PrimaryTextField( + modifier = modifier.fillMaxWidth(), + value = repeatPassword, + onValueChange = { repeatPassword = it }, + label = repeatPasswordLabel, + placeholder = repeatPasswordLabel, + isPasswordText = true, + keyboardOptions = + KeyboardOptions( + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Done, + ), + ) + + Spacer(modifier = modifier.height(MPadding)) + + CancelAndOkButtonRow( + modifier = modifier, + cancelButtonClick = onDismiss, + okButtonClick = { onEncrypt(keyLabel.text, password.text) }, + okButtonEnabled = password.text.isNotEmpty() && password.text == repeatPassword.text, + cancelButtonTitle = R.string.cancel_button, + okButtonTitle = R.string.encrypt_button, + cancelButtonContentDescription = + stringResource(R.string.cancel_button).lowercase(), + okButtonContentDescription = + stringResource(R.string.encrypt_button).lowercase(), + cancelButtonTestTag = "encryptPasswordDialogCancelButton", + okButtonTestTag = "encryptPasswordDialogEncryptButton", + ) + } + } + } +} + +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +fun EncryptPasswordDialogPreview() { + RIADigiDocTheme { + EncryptPasswordDialog() + } +} diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/CryptoDataFilesLocked.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/CryptoDataFilesLocked.kt index 6640411e4..722a776bd 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/CryptoDataFilesLocked.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/CryptoDataFilesLocked.kt @@ -49,6 +49,7 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.tooling.preview.Preview import ee.ria.DigiDoc.R +import ee.ria.DigiDoc.ui.theme.Dimensions.MSPadding import ee.ria.DigiDoc.ui.theme.Dimensions.SPadding import ee.ria.DigiDoc.ui.theme.Dimensions.XSPadding import ee.ria.DigiDoc.ui.theme.Dimensions.iconSizeXXS @@ -99,7 +100,7 @@ fun CryptoDataFilesLocked(modifier: Modifier = Modifier) { Spacer(modifier = modifier.width(SPadding)) - Column(modifier = modifier.weight(1f)) { + Column(modifier = modifier.weight(1f).padding(bottom = MSPadding)) { Text( modifier = modifier diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/PrimaryTextField.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/PrimaryTextField.kt index 885bdd3d7..8c6d766c8 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/PrimaryTextField.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/PrimaryTextField.kt @@ -35,6 +35,7 @@ import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -67,11 +68,14 @@ import androidx.compose.ui.text.style.TextAlign import ee.ria.DigiDoc.R import ee.ria.DigiDoc.ui.theme.Dimensions.MSPadding import ee.ria.DigiDoc.ui.theme.Dimensions.XSPadding +import ee.ria.DigiDoc.utils.accessibility.AccessibilityUtil.Companion.getAccessibilityEventType import ee.ria.DigiDoc.utils.accessibility.AccessibilityUtil.Companion.isTalkBackEnabled +import ee.ria.DigiDoc.utils.accessibility.AccessibilityUtil.Companion.sendAccessibilityEvent import ee.ria.DigiDoc.utils.extensions.notAccessible import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlin.time.Duration.Companion.milliseconds @Composable fun PrimaryTextField( @@ -111,6 +115,12 @@ fun PrimaryTextField( val clearButtonText = stringResource(R.string.clear_text) val buttonName = stringResource(R.string.button_name) + LaunchedEffect(errorText) { + if (errorText.isNotEmpty()) { + sendAccessibilityEvent(context, getAccessibilityEventType(), errorText) + } + } + Column(modifier = modifier) { Row( modifier = Modifier.fillMaxWidth(), @@ -128,11 +138,18 @@ fun PrimaryTextField( editingStarted = false } }.semantics { - if (readDigitByDigit && value.text.isNotEmpty() && value.text.all { it.isDigit() }) { - contentDescription = value.text.split("").joinToString(" ") - } else if (isPasswordText) { - contentDescription = "" - } + contentDescription = + if (readDigitByDigit && value.text.isNotEmpty() && value.text.all { it.isDigit() }) { + value.text.split("").joinToString(" ") + } else if (isPasswordText) { + "" + } else { + if (description.isNotEmpty()) { + "$label, $description: ${value.text}" + } else { + "$label: ${value.text}" + } + } testTagsAsResourceId = true }.then(if (testTag.isNotEmpty()) Modifier.testTag(testTag) else Modifier), enabled = enabled, @@ -199,7 +216,7 @@ fun PrimaryTextField( scope.launch(Main) { focusRequester.requestFocus() focusManager.clearFocus() - delay(200) + delay(200.milliseconds) focusRequester.requestFocus() } }) { diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/Recipient.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/Recipient.kt index 16ac64c8e..e2f23af1a 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/Recipient.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/shared/Recipient.kt @@ -85,28 +85,27 @@ fun Recipient( val recipientText = stringResource(id = R.string.crypto_recipient_title) val buttonName = stringResource(id = R.string.button_name) + val isPasswordRecipient = recipient.certType == CertType.PasswordType val nameText = - if (PersonalCodeValidator.isPersonalCodeValid(recipient.identifier)) { - formatName(recipient.surname, recipient.givenName, recipient.identifier) - } else { - formatCompanyName(recipient.identifier, recipient.serialNumber) + when { + isPasswordRecipient -> recipient.identifier + PersonalCodeValidator.isPersonalCodeValid(recipient.identifier) -> + formatName(recipient.surname, recipient.givenName, recipient.identifier) + else -> formatCompanyName(recipient.identifier, recipient.serialNumber) } - val certTypeText = getRecipientCertTypeText(LocalContext.current, recipient.certType) + val certTypeText = getRecipientCertTypeText(context, recipient.certType) val certValidTo = - recipient.validTo - ?.let { - dateFormat.format( - it, - ) - }?.let { - stringResource( - R.string.crypto_cert_valid_to, - it, - ) - } ?: "" + if (isPasswordRecipient) { + "" + } else { + recipient.validTo + ?.let { dateFormat.format(it) } + ?.let { stringResource(R.string.crypto_cert_valid_to, it) } + ?: "" + } val iconRes = - if (recipient.surname.isNullOrEmpty() && recipient.givenName.isNullOrEmpty()) { + if (isPasswordRecipient || (recipient.surname.isNullOrEmpty() && recipient.givenName.isNullOrEmpty())) { R.drawable.ic_m3_domain_48dp_wght400 } else { R.drawable.ic_m3_encrypted_48dp_wght400 diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/SigningNavigation.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/SigningNavigation.kt index 7a958d7f0..1d7ea0967 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/SigningNavigation.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/SigningNavigation.kt @@ -26,7 +26,6 @@ import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent import android.content.res.Configuration -import android.util.Log import android.widget.Toast import androidx.activity.compose.BackHandler import androidx.activity.compose.rememberLauncherForActivityResult diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/utils/libdigidoc/RecipientCertTypeUtil.kt b/app/src/main/kotlin/ee/ria/DigiDoc/utils/libdigidoc/RecipientCertTypeUtil.kt index 5703d7cd5..e0fa0cae5 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/utils/libdigidoc/RecipientCertTypeUtil.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/utils/libdigidoc/RecipientCertTypeUtil.kt @@ -38,5 +38,6 @@ object RecipientCertTypeUtil { CertType.MobileIDType -> context.getString(R.string.crypto_container_cert_type_mobile_id_type) CertType.SmartIDType -> context.getString(R.string.crypto_container_cert_type_smart_id_type) CertType.ESealType -> context.getString(R.string.crypto_container_cert_type_e_seal_type) + CertType.PasswordType -> context.getString(R.string.crypto_recipient_type_password) } } diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/EncryptViewModel.kt b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/EncryptViewModel.kt index 32a3891f9..325a5e45f 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/EncryptViewModel.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/EncryptViewModel.kt @@ -34,6 +34,8 @@ import ee.ria.DigiDoc.R import ee.ria.DigiDoc.common.Constant.CDOC1_EXTENSION import ee.ria.DigiDoc.cryptolib.CDOC2Settings import ee.ria.DigiDoc.cryptolib.CryptoContainer +import ee.ria.DigiDoc.domain.model.settings.CDOCSetting +import ee.ria.DigiDoc.domain.preferences.DataStore import ee.ria.DigiDoc.domain.repository.fileopening.FileOpeningRepository import ee.ria.DigiDoc.domain.repository.siva.SivaRepository import ee.ria.DigiDoc.libdigidoclib.SignedContainer @@ -57,9 +59,12 @@ class EncryptViewModel private val contentResolver: ContentResolver, private val fileOpeningRepository: FileOpeningRepository, private val cdoc2Settings: CDOC2Settings, + private val dataStore: DataStore, ) : ViewModel() { private val logTag = javaClass.simpleName + val cdocSetting: CDOCSetting = dataStore.getCdocSetting(false) + private val _shouldResetCryptoContainer = MutableLiveData(false) val shouldResetCryptoContainer: LiveData = _shouldResetCryptoContainer diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index bb14e4803..bde5e727d 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -172,6 +172,21 @@ Krüpteeritud failid Ümbriku nimi Krüpteerimiseks tuleb lisada vähemalt üks adressaat – isik (isikukood) või asutus (registrikood, nimi või nimeosa), kellel on õigus andmeid dekrüpteerida. Lisa enda isikukood, kui soovid hiljem ümbrikku dekrüpteerida. + Krüpteeri adressaadi alusel + Krüpteeri parooliga + Parooliga krüpteerimine on mõeldud pikaajaliseks säilitamiseks. Parooli ei saa muuta ega taastada. + Võtme silt + Adressaadi nimi või ID + Salvestage parool kindlasti turvalisse kohta – ilma paroolita ei saa faili enam avada. + Dokumendi parool + Parooli nõuded + Pikkus: 20 – 64 tähemärki + Pikkus: 20 kuni 64 tähemärki + Sisaldab vähemalt ühte numbrit (0 – 9) + Sisaldab vähemalt ühte numbrit (0 kuni 9) + Sisaldab vähemalt ühte suurtähte + Sisaldab vähemalt ühte väiketähte + Korrake parooli Leia adressaat… Katkesta adressaadi otsing Adressaati ei leitud diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bf64e3f8b..6b58a2553 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -172,6 +172,21 @@ Encrypted files Container name For encryption, at least one recipient must be included - the person (personal identification number) or authority (registration number, name or part of the name) authorised to decrypt the data. Add your own personal identification number if you want to decrypt the envelope later. + Encrypt based on recipient + Encrypt with password + Password encryption is intended for long-term storage. The password cannot be changed or recovered. + Key label + Recipient name or id + Be sure to save the password in a secure place - without the password, you won\'t be able to open the file again. + Document password + Password requirements + Length: 20 – 64 characters + Length: 20 to 64 characters + Contains at least one number (0 – 9) + Contains at least one number (0 to 9) + Contains at least one uppercase letter + Contains at least one lowercase letter + Repeat password Search recipients… Cancel search recipients No recipients found diff --git a/crypto-lib/src/main/kotlin/ee/ria/DigiDoc/cryptolib/Addressee.kt b/crypto-lib/src/main/kotlin/ee/ria/DigiDoc/cryptolib/Addressee.kt index 500246817..32ce1500c 100644 --- a/crypto-lib/src/main/kotlin/ee/ria/DigiDoc/cryptolib/Addressee.kt +++ b/crypto-lib/src/main/kotlin/ee/ria/DigiDoc/cryptolib/Addressee.kt @@ -44,7 +44,7 @@ class Addressee( var certType: CertType, var validTo: Date?, var concatKDFAlgorithmURI: String?, -): Serializable { +) : Serializable { constructor(cn: String, sn: String, certType: CertType, validTo: Date?, data: ByteArray) : this( data = data, identifier = "", diff --git a/crypto-lib/src/main/kotlin/ee/ria/DigiDoc/cryptolib/CertType.kt b/crypto-lib/src/main/kotlin/ee/ria/DigiDoc/cryptolib/CertType.kt index d4b957b3c..61aea0a20 100644 --- a/crypto-lib/src/main/kotlin/ee/ria/DigiDoc/cryptolib/CertType.kt +++ b/crypto-lib/src/main/kotlin/ee/ria/DigiDoc/cryptolib/CertType.kt @@ -29,6 +29,7 @@ enum class CertType { MobileIDType, SmartIDType, ESealType, + PasswordType, } fun certType(policies: List): CertType { diff --git a/crypto-lib/src/main/kotlin/ee/ria/DigiDoc/cryptolib/CryptoContainer.kt b/crypto-lib/src/main/kotlin/ee/ria/DigiDoc/cryptolib/CryptoContainer.kt index e9e5fdbe1..174fb2e19 100644 --- a/crypto-lib/src/main/kotlin/ee/ria/DigiDoc/cryptolib/CryptoContainer.kt +++ b/crypto-lib/src/main/kotlin/ee/ria/DigiDoc/cryptolib/CryptoContainer.kt @@ -55,6 +55,7 @@ import ee.ria.cdoc.ILogger import ee.ria.cdoc.Lock import ee.ria.cdoc.NetworkBackend import ee.ria.cdoc.Recipient +import ee.ria.cdoc.Recipient.parseLabel import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.withContext import org.apache.commons.io.FilenameUtils @@ -196,9 +197,24 @@ class CryptoContainer Addressee(lock.label, lock.getBytes(Lock.Params.RCPT_KEY), ""), ) } else if (lock.isSymmetric) { - addressees.add( - Addressee(lock.label, "", CertType.UnknownType, null, ByteArray(0)), - ) + if (lock.type == Lock.Type.PASSWORD) { + addressees.add( + Addressee( + data = ByteArray(0), + identifier = parseLabel(lock.label.orEmpty())["label"].orEmpty(), + serialNumber = null, + givenName = null, + surname = null, + certType = CertType.PasswordType, + validTo = null, + concatKDFAlgorithmURI = null, + ), + ) + } else { + addressees.add( + Addressee(lock.label, "", CertType.UnknownType, null, ByteArray(0)), + ) + } } else { addressees.add(Addressee("Unknown capsule", ByteArray(0), "")) } diff --git a/crypto-lib/src/main/res/values-et/strings.xml b/crypto-lib/src/main/res/values-et/strings.xml index b0e9a8cb6..014cefdfc 100644 --- a/crypto-lib/src/main/res/values-et/strings.xml +++ b/crypto-lib/src/main/res/values-et/strings.xml @@ -7,4 +7,5 @@ Mobile-ID Smart-ID Krüpteerimissertifikaat + Parool \ No newline at end of file diff --git a/crypto-lib/src/main/res/values/strings.xml b/crypto-lib/src/main/res/values/strings.xml index 83706e0e2..f5789599d 100644 --- a/crypto-lib/src/main/res/values/strings.xml +++ b/crypto-lib/src/main/res/values/strings.xml @@ -7,4 +7,5 @@ Mobile-ID Smart-ID Certificate for Encryption + Password \ No newline at end of file