diff --git a/app/src/androidTest/kotlin/ee/ria/DigiDoc/domain/preferences/DataStoreTest.kt b/app/src/androidTest/kotlin/ee/ria/DigiDoc/domain/preferences/DataStoreTest.kt index 84f048b2f..ca712ae53 100644 --- a/app/src/androidTest/kotlin/ee/ria/DigiDoc/domain/preferences/DataStoreTest.kt +++ b/app/src/androidTest/kotlin/ee/ria/DigiDoc/domain/preferences/DataStoreTest.kt @@ -315,6 +315,32 @@ class DataStoreTest { assertEquals(false, result) } + @Test + fun dataStore_getSettingsDefaultLTA_returnFalseByDefault() { + val result = dataStore.getSettingsDefaultLTA() + + assertFalse(result) + } + + @Test + fun dataStore_setSettingsDefaultLTA_returnTrueWhenEnabled() { + dataStore.setSettingsDefaultLTA(true) + + val result = dataStore.getSettingsDefaultLTA() + + assertTrue(result) + } + + @Test + fun dataStore_setSettingsDefaultLTA_returnFalseWhenDisabled() { + dataStore.setSettingsDefaultLTA(true) + dataStore.setSettingsDefaultLTA(false) + + val result = dataStore.getSettingsDefaultLTA() + + assertFalse(result) + } + @Test fun dataStore_getSettingsAllowScreenshots_success() { val result = dataStore.getSettingsAllowScreenshots() @@ -561,7 +587,7 @@ class DataStoreTest { fun dataStore_getLibdigidocppVersion_success() { val result = dataStore.getLibdigidocppVersion() - assertEquals("4.3.0.40", result) + assertEquals("4.4.0.48", result) } @Test diff --git a/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/SigningViewModelTest.kt b/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/SigningViewModelTest.kt index 422056d29..f31ba84f4 100644 --- a/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/SigningViewModelTest.kt +++ b/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/SigningViewModelTest.kt @@ -65,7 +65,6 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock -import org.mockito.Mockito.atLeastOnce import org.mockito.Mockito.mock import org.mockito.Mockito.verify import org.mockito.Mockito.`when` @@ -73,6 +72,7 @@ import org.mockito.MockitoAnnotations import org.mockito.junit.MockitoJUnitRunner import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.atLeastOnce import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import java.io.File @@ -545,6 +545,13 @@ class SigningViewModelTest { assertFalse(isContainerWithTimestamps) } + @Test + fun signingViewModel_isContainerWithTimestamps_returnFalseWhenContainerIsNull() { + val isContainerWithTimestamps = viewModel.isContainerWithTimestamps(null) + + assertFalse(isContainerWithTimestamps) + } + @Test fun signingViewModel_getMimetype_success() = runTest { @@ -596,4 +603,50 @@ class SigningViewModelTest { assertEquals("text/plain", mimetype) } + + @Test + fun signingViewModel_isExtendSignaturesButtonShown_returnTrueWithSignedContainerNotNested() = + runTest { + val file = getResourceFileAsFile(context, "example.asice", R.raw.example) + val container = SignedContainer.openOrCreate(context, file, listOf(file), true) + + val isExtendSignaturesButtonShown = viewModel.isExtendSignaturesButtonShown(container, false) + + assertTrue(isExtendSignaturesButtonShown) + } + + @Test + fun signingViewModel_isExtendSignaturesButtonShown_returnFalseWhenContainerIsNull() { + val isExtendSignaturesButtonShown = viewModel.isExtendSignaturesButtonShown(null, false) + + assertFalse(isExtendSignaturesButtonShown) + } + + @Test + fun signingViewModel_isExtendSignaturesButtonShown_returnFalseWhenContainerIsUnsigned() = + runTest { + val file = + getResourceFileAsFile( + context, + "example_no_signatures.asice", + R.raw.example_no_signatures, + ) + val container = SignedContainer.openOrCreate(context, file, listOf(file), true) + + val isExtendSignaturesButtonShown = viewModel.isExtendSignaturesButtonShown(container, false) + + assertFalse(isExtendSignaturesButtonShown) + } + + @Test + fun signingViewModel_isExtendSignaturesButtonShown_returnFalseWhenContainerIsNested() = + runTest { + val file = getResourceFileAsFile(context, "example.asice", R.raw.example) + val container = SignedContainer.openOrCreate(context, file, listOf(file), true) + + val isExtendSignaturesButtonShown = viewModel.isExtendSignaturesButtonShown(container, true) + + assertFalse(isExtendSignaturesButtonShown) + } + } diff --git a/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/shared/SharedSettingsViewModelTest.kt b/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/shared/SharedSettingsViewModelTest.kt index f2183ebdf..fbc40069f 100644 --- a/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/shared/SharedSettingsViewModelTest.kt +++ b/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/shared/SharedSettingsViewModelTest.kt @@ -161,6 +161,7 @@ class SharedSettingsViewModelTest { assertEquals(DEFAULT_UUID_VALUE, dataStore.getSettingsUUID()) assertEquals("", dataStore.getSettingsTSAUrl()) assertFalse(dataStore.getSettingsAskRoleAndAddress()) + assertFalse(dataStore.getSettingsDefaultLTA()) assertFalse(dataStore.getIsTsaCertificateViewVisible()) assertEquals("", dataStore.getTSACertName()) @@ -182,6 +183,15 @@ class SharedSettingsViewModelTest { assertEquals("", dataStore.getProxyPassword()) } + @Test + fun sharedSettingsViewModel_resetToDefaultSettings_resetsDefaultLTAToFalse() { + dataStore.setSettingsDefaultLTA(true) + + viewModel.resetToDefaultSettings() + + assertFalse(dataStore.getSettingsDefaultLTA()) + } + @Test fun sharedSettingsViewModel_saveProxySettings_savesManualProxySettings() { dataStore.setProxySetting(ProxySetting.MANUAL_PROXY) diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/domain/preferences/DataStore.kt b/app/src/main/kotlin/ee/ria/DigiDoc/domain/preferences/DataStore.kt index f9b142dc7..3f856b054 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/domain/preferences/DataStore.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/domain/preferences/DataStore.kt @@ -452,6 +452,21 @@ class DataStore } } + fun getSettingsDefaultLTA(): Boolean = + preferences.getBoolean( + resources.getString(R.string.main_settings_default_lta_key), + false, + ) + + fun setSettingsDefaultLTA(isEnabled: Boolean) { + preferences.edit { + putBoolean( + resources.getString(R.string.main_settings_default_lta_key), + isEnabled, + ) + } + } + fun getRoles(): String = preferences.getString( resources.getString(R.string.main_settings_role_key), @@ -685,13 +700,14 @@ class DataStore ) ?: "" fun setProxyPassword(password: String) { - getEncryptedPreferences(context)?.edit { + if (getEncryptedPreferences(context)?.edit { putString( resources.getString(ee.ria.DigiDoc.network.R.string.main_settings_proxy_password_key), password, ) + } == null) { + errorLog(logTag, "Unable to set proxy password") } - errorLog(logTag, "Unable to set proxy password") } fun getProxyPassword(): String { diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/EncryptionServicesSettingsScreen.kt b/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/EncryptionServicesSettingsScreen.kt index 9fe83e629..ce6c90e33 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/EncryptionServicesSettingsScreen.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/EncryptionServicesSettingsScreen.kt @@ -23,7 +23,6 @@ package ee.ria.DigiDoc.fragment.screen import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement @@ -31,7 +30,6 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -43,12 +41,9 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material3.BasicAlertDialog -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.RadioButton import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState @@ -64,10 +59,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope 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.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag @@ -89,6 +82,7 @@ import ee.ria.DigiDoc.configuration.provider.ConfigurationProvider.CDOC2Conf import ee.ria.DigiDoc.domain.model.settings.CDOCSetting import ee.ria.DigiDoc.ui.component.menu.SettingsMenuBottomSheet import ee.ria.DigiDoc.ui.component.settings.SettingsSwitchItem +import ee.ria.DigiDoc.ui.component.settings.shared.SettingsRadioCard import ee.ria.DigiDoc.ui.component.shared.InvisibleElement import ee.ria.DigiDoc.ui.component.shared.PrimaryTextField import ee.ria.DigiDoc.ui.component.shared.TopBar @@ -97,17 +91,13 @@ import ee.ria.DigiDoc.ui.component.support.textFieldValueSaver import ee.ria.DigiDoc.ui.theme.Dimensions.LPadding import ee.ria.DigiDoc.ui.theme.Dimensions.MSPadding import ee.ria.DigiDoc.ui.theme.Dimensions.SPadding -import ee.ria.DigiDoc.ui.theme.Dimensions.XSBorder import ee.ria.DigiDoc.ui.theme.Dimensions.XSPadding -import ee.ria.DigiDoc.ui.theme.buttonRoundedCornerShape import ee.ria.DigiDoc.utils.Route -import ee.ria.DigiDoc.utils.extensions.notAccessible import ee.ria.DigiDoc.utils.snackbar.SnackBarManager import ee.ria.DigiDoc.viewmodel.EncryptionServicesViewModel import ee.ria.DigiDoc.viewmodel.shared.SharedCertificateViewModel import ee.ria.DigiDoc.viewmodel.shared.SharedMenuViewModel import ee.ria.DigiDoc.viewmodel.shared.SharedSettingsViewModel -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.launch @@ -131,6 +121,7 @@ fun EncryptionServicesSettingsScreen( val context = LocalContext.current val snackBarHostState = remember { SnackbarHostState() } val snackBarScope = rememberCoroutineScope() + val scope = rememberCoroutineScope() val messages by SnackBarManager.messages.collectAsState(emptyList()) @@ -270,7 +261,7 @@ fun EncryptionServicesSettingsScreen( navController.popBackStack() return@rememberLauncherForActivityResult } - CoroutineScope(Dispatchers.IO).launch { + scope.launch(Dispatchers.IO) { sharedSettingsViewModel.handleCryptoCertFile(uri) withContext(Main) { sharedSettingsViewModel.updateCryptoCertData(context) @@ -340,100 +331,24 @@ fun EncryptionServicesSettingsScreen( .padding(top = SPadding) .verticalScroll(rememberScrollState()), ) { - Card( - modifier = - modifier - .fillMaxWidth() - .padding(top = XSPadding, bottom = SPadding), - shape = buttonRoundedCornerShape, - border = - BorderStroke( - width = XSBorder, - color = MaterialTheme.colorScheme.onSurface, - ), - colors = CardDefaults.cardColors(containerColor = Color.Transparent), - ) { - Row( - modifier = - modifier - .fillMaxWidth() - .padding(SPadding) - .clickable { - encryptionServicesViewModel.setCdocSetting(CDOCSetting.CDOC1) - }, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = useCDOC1Label, - modifier = - modifier - .weight(1f) - .notAccessible(), - ) - RadioButton( - modifier = - modifier - .semantics { - contentDescription = useCDOC1Label - }, - selected = cdocSetting == CDOCSetting.CDOC1, - onClick = { - encryptionServicesViewModel.setCdocSetting(CDOCSetting.CDOC1) - }, - ) - } - } + SettingsRadioCard( + modifier = modifier, + label = useCDOC1Label, + selected = cdocSetting == CDOCSetting.CDOC1, + onClick = { + encryptionServicesViewModel.setCdocSetting(CDOCSetting.CDOC1) + }, + ) - Card( - modifier = - modifier - .fillMaxWidth() - .padding(top = XSPadding, bottom = SPadding), - shape = buttonRoundedCornerShape, - border = - BorderStroke( - width = XSBorder, - color = MaterialTheme.colorScheme.onSurface, - ), - colors = CardDefaults.cardColors(containerColor = Color.Transparent), + SettingsRadioCard( + modifier = modifier, + label = useCDOC2Label, + selected = cdocSetting == CDOCSetting.CDOC2, + onClick = { + encryptionServicesViewModel.setCdocSetting(CDOCSetting.CDOC2) + }, ) { - Column( - modifier = - modifier - .padding(SPadding) - .fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Row( - modifier = - modifier - .clickable { - encryptionServicesViewModel.setCdocSetting(CDOCSetting.CDOC2) - }, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = useCDOC2Label, - style = MaterialTheme.typography.bodyLarge, - modifier = - modifier - .weight(1f) - .notAccessible(), - ) - RadioButton( - modifier = - modifier - .semantics { - contentDescription = useCDOC2Label - }, - selected = cdocSetting == CDOCSetting.CDOC2, - onClick = { - encryptionServicesViewModel.setCdocSetting(CDOCSetting.CDOC2) - }, - ) - } - - if (cdocSetting == CDOCSetting.CDOC2) { + if (cdocSetting == CDOCSetting.CDOC2) { Spacer(modifier = modifier.height(LPadding)) SettingsSwitchItem( @@ -693,7 +608,6 @@ fun EncryptionServicesSettingsScreen( } } } - } } } diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/ProxyServicesSettingsScreen.kt b/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/ProxyServicesSettingsScreen.kt index 71a59721a..0c487f022 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/ProxyServicesSettingsScreen.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/ProxyServicesSettingsScreen.kt @@ -21,8 +21,6 @@ package ee.ria.DigiDoc.fragment.screen -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi @@ -33,12 +31,9 @@ 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.Card -import androidx.compose.material3.CardDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.RadioButton import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState @@ -56,12 +51,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource @@ -80,23 +71,18 @@ import ee.ria.DigiDoc.R import ee.ria.DigiDoc.network.proxy.ManualProxy import ee.ria.DigiDoc.network.proxy.ProxySetting import ee.ria.DigiDoc.ui.component.menu.SettingsMenuBottomSheet +import ee.ria.DigiDoc.ui.component.settings.shared.SettingsRadioCard import ee.ria.DigiDoc.ui.component.shared.InvisibleElement import ee.ria.DigiDoc.ui.component.shared.PrimaryTextField import ee.ria.DigiDoc.ui.component.shared.TopBar import ee.ria.DigiDoc.ui.component.support.textFieldValueSaver -import ee.ria.DigiDoc.ui.theme.Dimensions.LPadding import ee.ria.DigiDoc.ui.theme.Dimensions.SPadding -import ee.ria.DigiDoc.ui.theme.Dimensions.XSBorder import ee.ria.DigiDoc.ui.theme.Dimensions.XSPadding -import ee.ria.DigiDoc.ui.theme.buttonRoundedCornerShape -import ee.ria.DigiDoc.utils.accessibility.AccessibilityUtil.Companion.isTalkBackEnabled -import ee.ria.DigiDoc.utils.extensions.notAccessible import ee.ria.DigiDoc.utils.snackbar.SnackBarManager import ee.ria.DigiDoc.utils.snackbar.SnackBarManager.showMessage import ee.ria.DigiDoc.viewmodel.shared.SharedMenuViewModel import ee.ria.DigiDoc.viewmodel.shared.SharedSettingsViewModel import kotlinx.coroutines.Dispatchers.Main -import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -109,13 +95,6 @@ fun ProxyServicesSettingsScreen( navController: NavHostController, ) { val context = LocalContext.current - val focusManager = LocalFocusManager.current - val scope = rememberCoroutineScope() - - val hostFocusRequester = remember { FocusRequester() } - val portFocusRequester = remember { FocusRequester() } - val usernameFocusRequester = remember { FocusRequester() } - val passwordFocusRequester = remember { FocusRequester() } val isSettingsMenuBottomSheetVisible = rememberSaveable { mutableStateOf(false) } @@ -193,7 +172,6 @@ fun ProxyServicesSettingsScreen( val systemProxyText = stringResource(R.string.main_settings_proxy_use_system) val manualProxyText = stringResource(R.string.main_settings_proxy_manual) val proxyCheckConnectionText = stringResource(R.string.main_settings_proxy_check_connection) - val clearButtonText = stringResource(R.string.clear_text) val buttonName = stringResource(id = R.string.button_name) LaunchedEffect(messages) { @@ -254,332 +232,139 @@ fun ProxyServicesSettingsScreen( .padding(SPadding) .verticalScroll(rememberScrollState()), ) { - Card( - modifier = - modifier - .fillMaxWidth() - .padding(top = XSPadding, bottom = SPadding), - shape = buttonRoundedCornerShape, - border = - BorderStroke( - width = XSBorder, - color = MaterialTheme.colorScheme.onSurface, - ), - colors = CardDefaults.cardColors(containerColor = Color.Transparent), + SettingsRadioCard( + modifier = modifier, + label = noProxyText, + selected = settingsProxyChoice.value == ProxySetting.NO_PROXY.name, + onClick = { + settingsProxyChoice.value = ProxySetting.NO_PROXY.name + setProxySetting(ProxySetting.NO_PROXY) + }, + ) + + SettingsRadioCard( + modifier = modifier, + label = systemProxyText, + selected = settingsProxyChoice.value == ProxySetting.SYSTEM_PROXY.name, + onClick = { + settingsProxyChoice.value = ProxySetting.SYSTEM_PROXY.name + setProxySetting(ProxySetting.SYSTEM_PROXY) + }, + ) + + SettingsRadioCard( + modifier = modifier, + label = manualProxyText, + selected = settingsProxyChoice.value == ProxySetting.MANUAL_PROXY.name, + onClick = { + settingsProxyChoice.value = ProxySetting.MANUAL_PROXY.name + setProxySetting(ProxySetting.MANUAL_PROXY) + }, ) { - Row( - modifier = - modifier - .fillMaxWidth() - .padding(SPadding) - .clickable { - settingsProxyChoice.value = ProxySetting.NO_PROXY.name - setProxySetting(ProxySetting.NO_PROXY) - sharedSettingsViewModel.saveProxySettings(true, ManualProxy("", 80, "", "")) - }, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = noProxyText, - modifier = - modifier - .weight(1f) - .notAccessible(), - ) - RadioButton( - modifier = - modifier - .semantics { - contentDescription = noProxyText - }, - selected = settingsProxyChoice.value == ProxySetting.NO_PROXY.name, - onClick = { - settingsProxyChoice.value = ProxySetting.NO_PROXY.name - setProxySetting(ProxySetting.NO_PROXY) - sharedSettingsViewModel.saveProxySettings(true, ManualProxy("", 80, "", "")) + if (settingsProxyChoice.value == ProxySetting.MANUAL_PROXY.name) { + PrimaryTextField( + modifier = Modifier.padding(vertical = XSPadding), + value = proxyHost, + onValueChange = { + proxyHost = it + setProxyHost(it.text) }, + singleLine = true, + label = stringResource(R.string.main_settings_proxy_host), + enabled = settingsProxyChoice.value == ProxySetting.MANUAL_PROXY.name, + keyboardOptions = + KeyboardOptions.Default.copy( + imeAction = ImeAction.Next, + keyboardType = KeyboardType.Uri, + ), + testTag = "proxyServicesHostTextField", + removeIconTestTag = "proxyServicesHostRemoveIconButton", ) - } - } - Card( - modifier = - modifier - .fillMaxWidth() - .padding(top = XSPadding, bottom = SPadding), - shape = buttonRoundedCornerShape, - border = - BorderStroke( - width = XSBorder, - color = MaterialTheme.colorScheme.onSurface, - ), - colors = CardDefaults.cardColors(containerColor = Color.Transparent), - ) { - Row( - modifier = - modifier - .fillMaxWidth() - .padding(SPadding) - .clickable { - settingsProxyChoice.value = ProxySetting.SYSTEM_PROXY.name - setProxySetting(ProxySetting.SYSTEM_PROXY) - sharedSettingsViewModel.saveProxySettings(true, ManualProxy("", 80, "", "")) - }, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = systemProxyText, - modifier = - modifier - .weight(1f) - .notAccessible(), - ) - RadioButton( - modifier = - modifier - .semantics { - contentDescription = systemProxyText - }, - selected = settingsProxyChoice.value == ProxySetting.SYSTEM_PROXY.name, - onClick = { - settingsProxyChoice.value = ProxySetting.SYSTEM_PROXY.name - setProxySetting(ProxySetting.SYSTEM_PROXY) - sharedSettingsViewModel.saveProxySettings(true, ManualProxy("", 80, "", "")) + PrimaryTextField( + modifier = Modifier.padding(vertical = XSPadding), + value = proxyPort, + onValueChange = { + proxyPort = it + if (isValidPortNumber(it.text)) { + setProxyPort(it.text.toInt()) + } }, + singleLine = true, + label = stringResource(R.string.main_settings_proxy_port), + enabled = settingsProxyChoice.value == ProxySetting.MANUAL_PROXY.name, + keyboardOptions = + KeyboardOptions.Default.copy( + imeAction = ImeAction.Next, + keyboardType = KeyboardType.Number, + ), + isError = proxyPortErrorText.isNotEmpty(), + errorText = proxyPortErrorText, + testTag = "proxyServicesPortTextField", + removeIconTestTag = "proxyServicesPortRemoveIconButton", ) - } - } - Card( - modifier = - modifier - .fillMaxWidth() - .padding(top = XSPadding, bottom = SPadding), - shape = buttonRoundedCornerShape, - border = - BorderStroke( - width = XSBorder, - color = MaterialTheme.colorScheme.onSurface, - ), - colors = CardDefaults.cardColors(containerColor = Color.Transparent), - ) { - Row( - modifier = - modifier - .fillMaxWidth() - .padding(SPadding) - .clickable { - val proxyPortValue = proxyPort.text.toIntOrNull() ?: 80 - settingsProxyChoice.value = ProxySetting.MANUAL_PROXY.name - setProxySetting(ProxySetting.MANUAL_PROXY) - sharedSettingsViewModel.saveProxySettings( - false, - ManualProxy( - host = proxyHost.text, - port = proxyPortValue, - username = proxyUsername.text, - password = proxyPassword.text, - ), - ) - }, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = manualProxyText, - style = MaterialTheme.typography.bodyLarge, - modifier = - modifier - .weight(1f) - .notAccessible(), - ) - RadioButton( - modifier = - modifier - .semantics { - contentDescription = manualProxyText - }, - selected = settingsProxyChoice.value == ProxySetting.MANUAL_PROXY.name, - onClick = { - val proxyPortValue = proxyPort.text.toIntOrNull() ?: 80 - settingsProxyChoice.value = ProxySetting.MANUAL_PROXY.name - setProxySetting(ProxySetting.MANUAL_PROXY) - sharedSettingsViewModel.saveProxySettings( - false, - ManualProxy( - host = proxyHost.text, - port = proxyPortValue, - username = proxyUsername.text, - password = proxyPassword.text, - ), - ) + PrimaryTextField( + modifier = Modifier.padding(vertical = XSPadding), + value = proxyUsername, + onValueChange = { + proxyUsername = it + setProxyUsername(it.text) }, + singleLine = true, + label = stringResource(R.string.main_settings_proxy_username), + enabled = settingsProxyChoice.value == ProxySetting.MANUAL_PROXY.name, + keyboardOptions = + KeyboardOptions.Default.copy( + imeAction = ImeAction.Next, + keyboardType = KeyboardType.Text, + ), + testTag = "proxyServicesUsernameTextField", + removeIconTestTag = "proxyServicesUsernameRemoveIconButton", ) - } - - if (settingsProxyChoice.value == ProxySetting.MANUAL_PROXY.name) { - Column( - modifier = - modifier - .padding(horizontal = SPadding) - .padding(bottom = LPadding), - ) { - PrimaryTextField( - modifier = - Modifier - .focusRequester(hostFocusRequester) - .padding(vertical = XSPadding), - value = proxyHost, - onValueChange = { - proxyHost = it - setProxyHost(it.text) - }, - singleLine = true, - label = stringResource(R.string.main_settings_proxy_host), - enabled = settingsProxyChoice.value == ProxySetting.MANUAL_PROXY.name, - keyboardOptions = - KeyboardOptions.Default.copy( - imeAction = ImeAction.Next, - keyboardType = KeyboardType.Uri, - ), - onDone = { - portFocusRequester.requestFocus() - }, - testTag = "proxyServicesHostTextField", - removeIconTestTag = "proxyServicesHostRemoveIconButton", - ) - PrimaryTextField( - modifier = - Modifier - .focusRequester(portFocusRequester) - .padding(vertical = XSPadding), - value = proxyPort, - onValueChange = { - proxyPort = it - if (isValidPortNumber(it.text)) { - setProxyPort(it.text.toInt()) + PrimaryTextField( + modifier = Modifier.padding(vertical = XSPadding), + value = proxyPassword, + onValueChange = { + proxyPassword = it + setProxyPassword(it.text) + }, + singleLine = true, + label = stringResource(R.string.main_settings_proxy_password), + enabled = settingsProxyChoice.value == ProxySetting.MANUAL_PROXY.name, + isPasswordText = !passwordVisible, + trailingIcon = { + val image = + if (passwordVisible) { + ImageVector.vectorResource(id = R.drawable.ic_visibility) + } else { + ImageVector.vectorResource(id = R.drawable.ic_visibility_off) } - }, - singleLine = true, - label = stringResource(R.string.main_settings_proxy_port), - enabled = settingsProxyChoice.value == ProxySetting.MANUAL_PROXY.name, - keyboardOptions = - KeyboardOptions.Default.copy( - imeAction = ImeAction.Next, - keyboardType = KeyboardType.Number, - ), - onDone = { - usernameFocusRequester.requestFocus() - }, - isError = proxyPortErrorText.isNotEmpty(), - errorText = proxyPortErrorText, - testTag = "proxyServicesPortTextField", - removeIconTestTag = "proxyServicesPortRemoveIconButton", - ) - - PrimaryTextField( - modifier = - Modifier - .focusRequester(usernameFocusRequester) - .padding(vertical = XSPadding), - value = proxyUsername, - onValueChange = { - proxyUsername = it - setProxyUsername(it.text) - }, - singleLine = true, - label = stringResource(R.string.main_settings_proxy_username), - enabled = settingsProxyChoice.value == ProxySetting.MANUAL_PROXY.name, - keyboardOptions = - KeyboardOptions.Default.copy( - imeAction = ImeAction.Next, - keyboardType = KeyboardType.Text, - ), - onDone = { - passwordFocusRequester.requestFocus() - }, - testTag = "proxyServicesUsernameTextField", - removeIconTestTag = "proxyServicesUsernameRemoveIconButton", - ) - - Row( - modifier = - modifier - .fillMaxWidth(), - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically, - ) { - PrimaryTextField( - modifier = - Modifier - .focusRequester(passwordFocusRequester) - .weight(1f) - .padding(vertical = XSPadding), - value = proxyPassword, - onValueChange = { - proxyPassword = it - setProxyPassword(it.text) - }, - singleLine = true, - label = stringResource(R.string.main_settings_proxy_password), - enabled = settingsProxyChoice.value == ProxySetting.MANUAL_PROXY.name, - isPasswordText = !passwordVisible, - trailingIcon = { - val image = - if (passwordVisible) { - ImageVector.vectorResource(id = R.drawable.ic_visibility) - } else { - ImageVector.vectorResource(id = R.drawable.ic_visibility_off) - } - val description = - if (passwordVisible) { - stringResource( - id = R.string.hide_password, - ) - } else { - stringResource(id = R.string.show_password) - } - IconButton( - modifier = - modifier - .semantics { traversalIndex = 9f } - .testTag("proxyServicesPasswordVisibleButton"), - onClick = { passwordVisible = !passwordVisible }, - ) { - Icon(imageVector = image, description) - } - }, - keyboardOptions = - KeyboardOptions.Default.copy( - imeAction = ImeAction.Done, - keyboardType = KeyboardType.Password, - ), - testTag = "proxyServicesPasswordTextField", - ) - - if (isTalkBackEnabled(context) && proxyPassword.text.isNotEmpty()) { - IconButton(onClick = { - proxyPassword = TextFieldValue("") - scope.launch(Main) { - passwordFocusRequester.requestFocus() - focusManager.clearFocus() - delay(200) - passwordFocusRequester.requestFocus() - } - }) { - Icon( - modifier = - modifier - .semantics { - testTagsAsResourceId = true - }.testTag("proxyServicesPasswordRemoveIconButton"), - imageVector = ImageVector.vectorResource(R.drawable.ic_icon_remove), - contentDescription = "$clearButtonText $buttonName", - ) + val description = + if (passwordVisible) { + stringResource(id = R.string.hide_password) + } else { + stringResource(id = R.string.show_password) } + IconButton( + modifier = + modifier + .semantics { traversalIndex = 9f } + .testTag("mainSettingsProxyPasswordVisibleButton"), + onClick = { passwordVisible = !passwordVisible }, + ) { + Icon(imageVector = image, description) } - } - } + }, + keyboardOptions = + KeyboardOptions.Default.copy( + imeAction = ImeAction.Done, + keyboardType = KeyboardType.Password, + ), + testTag = "proxyServicesPasswordTextField", + removeIconTestTag = "proxyServicesPasswordRemoveIconButton", + ) } } diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/SigningServicesSettingsScreen.kt b/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/SigningServicesSettingsScreen.kt index 8280d21b8..8aab7c290 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/SigningServicesSettingsScreen.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/SigningServicesSettingsScreen.kt @@ -51,6 +51,7 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController import ee.ria.DigiDoc.R import ee.ria.DigiDoc.ui.component.menu.SettingsMenuBottomSheet +import ee.ria.DigiDoc.ui.component.settings.advanced.signingservices.LTAComponent import ee.ria.DigiDoc.ui.component.settings.advanced.signingservices.MobileIdAndSmartIdServicesComponent import ee.ria.DigiDoc.ui.component.settings.advanced.signingservices.TimestampServicesComponent import ee.ria.DigiDoc.ui.component.shared.InvisibleElement @@ -157,6 +158,14 @@ fun SigningServicesSettingsScreen( sharedSettingsViewModel = sharedSettingsViewModel, ) }, + Pair( + stringResource(R.string.main_settings_default_lta_tab_title), + ) { + LTAComponent( + modifier, + sharedSettingsViewModel = sharedSettingsViewModel, + ) + }, ), ) diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/ValidationServicesSettingsScreen.kt b/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/ValidationServicesSettingsScreen.kt index 59a7a5ccb..4336cd5da 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/ValidationServicesSettingsScreen.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/ValidationServicesSettingsScreen.kt @@ -23,13 +23,10 @@ package ee.ria.DigiDoc.fragment.screen import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -38,10 +35,7 @@ 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.Card -import androidx.compose.material3.CardDefaults import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.RadioButton import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState @@ -57,13 +51,9 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope 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.focus.FocusRequester -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription @@ -78,22 +68,18 @@ import androidx.navigation.NavHostController import ee.ria.DigiDoc.R import ee.ria.DigiDoc.network.siva.SivaSetting import ee.ria.DigiDoc.ui.component.menu.SettingsMenuBottomSheet +import ee.ria.DigiDoc.ui.component.settings.shared.SettingsRadioCard import ee.ria.DigiDoc.ui.component.shared.InvisibleElement import ee.ria.DigiDoc.ui.component.shared.PrimaryTextField import ee.ria.DigiDoc.ui.component.shared.TopBar import ee.ria.DigiDoc.ui.component.support.textFieldValueSaver import ee.ria.DigiDoc.ui.theme.Dimensions.LPadding import ee.ria.DigiDoc.ui.theme.Dimensions.SPadding -import ee.ria.DigiDoc.ui.theme.Dimensions.XSBorder -import ee.ria.DigiDoc.ui.theme.Dimensions.XSPadding -import ee.ria.DigiDoc.ui.theme.buttonRoundedCornerShape import ee.ria.DigiDoc.utils.Route -import ee.ria.DigiDoc.utils.extensions.notAccessible import ee.ria.DigiDoc.utils.snackbar.SnackBarManager import ee.ria.DigiDoc.viewmodel.shared.SharedCertificateViewModel import ee.ria.DigiDoc.viewmodel.shared.SharedMenuViewModel import ee.ria.DigiDoc.viewmodel.shared.SharedSettingsViewModel -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.launch @@ -109,11 +95,8 @@ fun ValidationServicesSettingsScreen( navController: NavHostController, ) { val context = LocalContext.current - val focusManager = LocalFocusManager.current val scope = rememberCoroutineScope() - val focusRequester = remember { FocusRequester() } - val snackBarHostState = remember { SnackbarHostState() } val snackBarScope = rememberCoroutineScope() @@ -157,7 +140,7 @@ fun ValidationServicesSettingsScreen( navController.popBackStack() return@rememberLauncherForActivityResult } - CoroutineScope(Dispatchers.IO).launch { + scope.launch(Dispatchers.IO) { sharedSettingsViewModel.handleSivaFile(uri) withContext(Main) { sharedSettingsViewModel.updateSivaData(settingsSivaServiceUrl.text, context) @@ -166,8 +149,6 @@ fun ValidationServicesSettingsScreen( }, ) - var urlText by remember { mutableStateOf(defaultSivaServiceUrl) } - val issuedToTitleText = stringResource(R.string.main_settings_timestamp_cert_issued_to_title) val validToTitleText = stringResource(R.string.main_settings_timestamp_cert_valid_to_title) val showCertificateButtonText = stringResource(R.string.main_settings_timestamp_cert_show_certificate_button) @@ -177,7 +158,6 @@ fun ValidationServicesSettingsScreen( val useDefaultAccessText = stringResource(R.string.main_settings_siva_default_access_title) val useManualAccessText = stringResource(R.string.main_settings_siva_default_manual_access_title) - val clearButtonText = stringResource(R.string.clear_text) val buttonName = stringResource(id = R.string.button_name) LaunchedEffect(messages) { @@ -248,207 +228,130 @@ fun ValidationServicesSettingsScreen( }, ) - Card( - modifier = - modifier - .fillMaxWidth() - .padding(top = XSPadding, bottom = SPadding), - shape = buttonRoundedCornerShape, - border = - BorderStroke( - width = XSBorder, - color = MaterialTheme.colorScheme.onSurface, - ), - colors = CardDefaults.cardColors(containerColor = Color.Transparent), + SettingsRadioCard( + modifier = modifier, + label = useDefaultAccessText, + selected = settingsSivaServiceChoice.value == SivaSetting.DEFAULT.name, + onClick = { + settingsSivaServiceChoice.value = SivaSetting.DEFAULT.name + setSivaSetting(SivaSetting.DEFAULT) + }, + ) + + SettingsRadioCard( + modifier = modifier, + label = useManualAccessText, + selected = settingsSivaServiceChoice.value == SivaSetting.MANUAL.name, + onClick = { + settingsSivaServiceChoice.value = SivaSetting.MANUAL.name + setSivaSetting(SivaSetting.MANUAL) + }, ) { - Row( - modifier = - modifier - .fillMaxWidth() - .padding(SPadding) - .clickable { - settingsSivaServiceChoice.value = SivaSetting.DEFAULT.name - setSivaSetting(SivaSetting.DEFAULT) - }, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = useDefaultAccessText, + if (settingsSivaServiceChoice.value == SivaSetting.MANUAL.name) { + PrimaryTextField( modifier = - modifier - .weight(1f) - .notAccessible(), + Modifier + .padding(vertical = LPadding), + value = settingsSivaServiceUrl, + onValueChange = { + settingsSivaServiceUrl = it + setSettingsSivaUrl(it.text) + }, + singleLine = true, + label = stringResource(R.string.main_settings_siva_service_url), + enabled = settingsSivaServiceChoice.value == SivaSetting.MANUAL.name, + keyboardOptions = + KeyboardOptions.Default.copy( + imeAction = ImeAction.Done, + keyboardType = KeyboardType.Uri, + ), + testTag = "validationServicesComponentTextField", + removeIconTestTag = "validationServicesRemoveIconButton", ) - RadioButton( + + Spacer(modifier = modifier.height(SPadding)) + + Text( modifier = modifier + .fillMaxWidth() .semantics { - contentDescription = useDefaultAccessText + heading() }, - selected = settingsSivaServiceChoice.value == SivaSetting.DEFAULT.name, - onClick = { - settingsSivaServiceChoice.value = SivaSetting.DEFAULT.name - setSivaSetting(SivaSetting.DEFAULT) - }, + text = stringResource(R.string.main_settings_siva_certificate_title), + style = MaterialTheme.typography.bodyLarge, ) - } - } - Card( - modifier = - modifier - .fillMaxWidth() - .padding(top = XSPadding, bottom = SPadding), - shape = buttonRoundedCornerShape, - border = - BorderStroke( - width = XSBorder, - color = MaterialTheme.colorScheme.onSurface, - ), - colors = CardDefaults.cardColors(containerColor = Color.Transparent), - ) { - Column( - modifier = - modifier - .padding(SPadding) - .fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Row( - modifier = - modifier - .clickable { - settingsSivaServiceChoice.value = SivaSetting.MANUAL.name - setSivaSetting(SivaSetting.MANUAL) - }, - verticalAlignment = Alignment.CenterVertically, - ) { + if (sivaCertificate != null) { Text( - text = useManualAccessText, - style = MaterialTheme.typography.bodyLarge, - modifier = - modifier - .weight(1f) - .notAccessible(), - ) - RadioButton( - modifier = - modifier - .semantics { - contentDescription = useManualAccessText - }, - selected = settingsSivaServiceChoice.value == SivaSetting.MANUAL.name, - onClick = { - settingsSivaServiceChoice.value = SivaSetting.MANUAL.name - setSivaSetting(SivaSetting.MANUAL) - }, + modifier = modifier.fillMaxWidth(), + text = "$issuedToTitleText $issuedTo", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, ) - } - if (settingsSivaServiceChoice.value == SivaSetting.MANUAL.name) { - PrimaryTextField( - modifier = - Modifier - .padding(vertical = LPadding), - value = settingsSivaServiceUrl, - onValueChange = { - settingsSivaServiceUrl = it - setSettingsSivaUrl(it.text) - }, - singleLine = true, - label = stringResource(R.string.main_settings_siva_service_url), - enabled = settingsSivaServiceChoice.value == SivaSetting.MANUAL.name, - keyboardOptions = - KeyboardOptions.Default.copy( - imeAction = ImeAction.Done, - keyboardType = KeyboardType.Uri, - ), - testTag = "validationServicesComponentTextField", - removeIconTestTag = "validationServicesRemoveIconButton", + Text( + modifier = modifier.fillMaxWidth(), + text = "$validToTitleText $validTo", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, ) - + } else { Text( - modifier = - modifier - .fillMaxWidth() - .semantics { - heading() - }, - text = stringResource(R.string.main_settings_siva_certificate_title), - style = MaterialTheme.typography.bodyLarge, + modifier = modifier.fillMaxWidth(), + text = noCertificateFoundText, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, ) + } - if (sivaCertificate != null) { - Text( - modifier = modifier.fillMaxWidth(), - text = "$issuedToTitleText $issuedTo", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - - Text( - modifier = modifier.fillMaxWidth(), - text = "$validToTitleText $validTo", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } else { - Text( - modifier = modifier.fillMaxWidth(), - text = noCertificateFoundText, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - - Spacer(modifier = modifier.height(SPadding)) + Spacer(modifier = modifier.height(SPadding)) - FlowRow( - modifier = modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.End, - verticalArrangement = Arrangement.Center, - ) { - if (sivaCertificate != null) { - TextButton(onClick = { - sivaCertificate?.let { - sharedCertificateViewModel.setCertificate( - it, - ) - navController.navigate( - Route.CertificateDetail.route, - ) - } - }) { - Text( - modifier = - modifier - .semantics { - contentDescription = - "$showCertificateButtonText $buttonName" - testTagsAsResourceId = true - }.testTag("validationServicesShowCertificateActionButton"), - text = showCertificateButtonText, - color = MaterialTheme.colorScheme.primary, + FlowRow( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + verticalArrangement = Arrangement.Center, + ) { + if (sivaCertificate != null) { + TextButton(onClick = { + sivaCertificate?.let { + sharedCertificateViewModel.setCertificate( + it, + ) + navController.navigate( + Route.CertificateDetail.route, ) } - } - - TextButton(onClick = { - filePicker.launch("*/*") }) { Text( modifier = modifier .semantics { contentDescription = - "$addCertificateButtonText $buttonName" + "$showCertificateButtonText $buttonName" testTagsAsResourceId = true - }.testTag("validationServicesAddCertificateActionButton"), - text = addCertificateButtonText, + }.testTag("validationServicesShowCertificateActionButton"), + text = showCertificateButtonText, color = MaterialTheme.colorScheme.primary, ) } } + + TextButton(onClick = { + filePicker.launch("*/*") + }) { + Text( + modifier = + modifier + .semantics { + contentDescription = + "$addCertificateButtonText $buttonName" + testTagsAsResourceId = true + }.testTag("validationServicesAddCertificateActionButton"), + text = addCertificateButtonText, + color = MaterialTheme.colorScheme.primary, + ) + } } } } 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 7b2b1b1b7..62160fbbf 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 @@ -1199,6 +1199,9 @@ private fun handleBackButtonClick( } } } else { + sharedContainerViewModel.resetSignedContainer() + sharedContainerViewModel.resetCryptoContainer() + sharedContainerViewModel.resetContainerNotifications() sharedContainerViewModel.clearContainers() encryptViewModel.handleBackButton() navController.navigateUp() diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/settings/advanced/signingservices/LTAComponent.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/settings/advanced/signingservices/LTAComponent.kt new file mode 100644 index 000000000..93d823fa1 --- /dev/null +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/settings/advanced/signingservices/LTAComponent.kt @@ -0,0 +1,97 @@ +/* + * 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.settings.advanced.signingservices + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.heading +import androidx.compose.ui.semantics.semantics +import ee.ria.DigiDoc.R +import ee.ria.DigiDoc.ui.component.settings.shared.SettingsRadioCard +import ee.ria.DigiDoc.ui.component.shared.InvisibleElement +import ee.ria.DigiDoc.ui.theme.Dimensions.SPadding +import ee.ria.DigiDoc.viewmodel.shared.SharedSettingsViewModel + +@Composable +fun LTAComponent( + modifier: Modifier = Modifier, + sharedSettingsViewModel: SharedSettingsViewModel, +) { + val getDefaultLTA = sharedSettingsViewModel.dataStore::getSettingsDefaultLTA + val setDefaultLTA = sharedSettingsViewModel.dataStore::setSettingsDefaultLTA + + val isEnabled = rememberSaveable { mutableStateOf(getDefaultLTA()) } + // No DisposableEffect reset needed: the boolean requires no URL cleanup on navigation away. + + val titleText = stringResource(R.string.main_settings_default_lta_tab_title) + val disabledText = stringResource(R.string.main_settings_default_lta_disabled) + val enabledText = stringResource(R.string.main_settings_default_lta_enabled) + + Column( + modifier = + modifier + .fillMaxSize() + .padding(SPadding) + .padding(top = SPadding), + ) { + Text( + text = titleText, + style = MaterialTheme.typography.titleLarge, + modifier = + Modifier + .padding(bottom = SPadding) + .semantics { + heading() + }, + ) + + SettingsRadioCard( + modifier = modifier, + label = disabledText, + selected = !isEnabled.value, + onClick = { + isEnabled.value = false + setDefaultLTA(false) + }, + ) + + SettingsRadioCard( + modifier = modifier, + label = enabledText, + selected = isEnabled.value, + onClick = { + isEnabled.value = true + setDefaultLTA(true) + }, + ) + + InvisibleElement(modifier = modifier) + } +} diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/settings/advanced/signingservices/MobileIdAndSmartIdServicesComponent.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/settings/advanced/signingservices/MobileIdAndSmartIdServicesComponent.kt index 5da2cb32b..c6791c5c9 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/settings/advanced/signingservices/MobileIdAndSmartIdServicesComponent.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/settings/advanced/signingservices/MobileIdAndSmartIdServicesComponent.kt @@ -21,8 +21,6 @@ package ee.ria.DigiDoc.ui.component.settings.advanced.signingservices -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi @@ -31,12 +29,9 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.RadioButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect @@ -50,14 +45,12 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId @@ -69,16 +62,13 @@ import androidx.compose.ui.text.input.TextFieldValue import ee.ria.DigiDoc.R import ee.ria.DigiDoc.common.Constant.Defaults.DEFAULT_UUID_VALUE import ee.ria.DigiDoc.domain.model.settings.UUIDSetting +import ee.ria.DigiDoc.ui.component.settings.shared.SettingsRadioCard import ee.ria.DigiDoc.ui.component.shared.InvisibleElement import ee.ria.DigiDoc.ui.component.shared.PrimaryTextField import ee.ria.DigiDoc.ui.component.support.textFieldValueSaver import ee.ria.DigiDoc.ui.theme.Dimensions.LPadding import ee.ria.DigiDoc.ui.theme.Dimensions.SPadding -import ee.ria.DigiDoc.ui.theme.Dimensions.XSBorder -import ee.ria.DigiDoc.ui.theme.Dimensions.XSPadding -import ee.ria.DigiDoc.ui.theme.buttonRoundedCornerShape import ee.ria.DigiDoc.utils.accessibility.AccessibilityUtil.Companion.isTalkBackEnabled -import ee.ria.DigiDoc.utils.extensions.notAccessible import ee.ria.DigiDoc.viewmodel.shared.SharedSettingsViewModel import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.delay @@ -103,7 +93,7 @@ fun MobileIdAndSmartIdServicesComponent( val setSettingsUuid = sharedSettingsViewModel.dataStore::setSettingsUUID val setUuidSetting = sharedSettingsViewModel.dataStore::setUuidSetting val defaultUuid = getSettingsUUID() - val settingsUuidChoice = remember { mutableStateOf(getUuidSetting().name) } + val settingsUuidChoice = rememberSaveable { mutableStateOf(getUuidSetting().name) } var settingsUuid by rememberSaveable(stateSaver = textFieldValueSaver) { mutableStateOf( TextFieldValue( @@ -112,8 +102,6 @@ fun MobileIdAndSmartIdServicesComponent( ), ) } - sharedSettingsViewModel.updateTsaData(settingsUuid.text, context) - val useDefaultAccessText = stringResource(R.string.main_settings_siva_default_access_title) val useManualAccessText = stringResource(R.string.main_settings_siva_default_manual_access_title) val accessToMobileAndSmartIdServicesText = stringResource(R.string.main_settings_uuid_title) @@ -138,116 +126,36 @@ fun MobileIdAndSmartIdServicesComponent( .padding(top = SPadding), ) { Text( - text = stringResource(R.string.main_settings_uuid_title), + text = accessToMobileAndSmartIdServicesText, style = MaterialTheme.typography.titleLarge, modifier = - modifier + Modifier .padding(bottom = SPadding) .semantics { heading() }, ) - Card( - modifier = - modifier - .fillMaxWidth() - .padding(top = XSPadding, bottom = SPadding), - shape = buttonRoundedCornerShape, - border = - BorderStroke( - width = XSBorder, - color = MaterialTheme.colorScheme.onSurface, - ), - colors = CardDefaults.cardColors(containerColor = Color.Transparent), - ) { - Row( - modifier = - modifier - .fillMaxWidth() - .padding(SPadding) - .clickable { - settingsUuidChoice.value = UUIDSetting.DEFAULT.name - setUuidSetting(UUIDSetting.DEFAULT) - }, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = useDefaultAccessText, - modifier = - modifier - .weight(1f) - .notAccessible(), - ) - RadioButton( - modifier = - modifier - .semantics { - contentDescription = useDefaultAccessText - }, - selected = settingsUuidChoice.value == UUIDSetting.DEFAULT.name, - onClick = { - settingsUuidChoice.value = UUIDSetting.DEFAULT.name - setUuidSetting(UUIDSetting.DEFAULT) - }, - ) - } - } + SettingsRadioCard( + modifier = modifier, + label = useDefaultAccessText, + selected = settingsUuidChoice.value == UUIDSetting.DEFAULT.name, + onClick = { + settingsUuidChoice.value = UUIDSetting.DEFAULT.name + setUuidSetting(UUIDSetting.DEFAULT) + }, + ) - Card( - modifier = - modifier - .fillMaxWidth() - .padding(top = XSPadding, bottom = SPadding), - shape = buttonRoundedCornerShape, - border = - BorderStroke( - width = XSBorder, - color = MaterialTheme.colorScheme.onSurface, - ), - colors = CardDefaults.cardColors(containerColor = Color.Transparent), + SettingsRadioCard( + modifier = modifier, + label = useManualAccessText, + selected = settingsUuidChoice.value == UUIDSetting.MANUAL.name, + onClick = { + settingsUuidChoice.value = UUIDSetting.MANUAL.name + setUuidSetting(UUIDSetting.MANUAL) + }, ) { - Column( - modifier = - modifier - .padding(SPadding) - .fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Row( - modifier = - modifier - .clickable { - settingsUuidChoice.value = UUIDSetting.MANUAL.name - setUuidSetting(UUIDSetting.MANUAL) - }, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = useManualAccessText, - style = MaterialTheme.typography.bodyLarge, - modifier = - modifier - .weight(1f) - .notAccessible(), - ) - RadioButton( - modifier = - modifier - .semantics { - contentDescription = useManualAccessText - }, - selected = settingsUuidChoice.value == UUIDSetting.MANUAL.name, - onClick = { - settingsUuidChoice.value = UUIDSetting.MANUAL.name - setUuidSetting(UUIDSetting.MANUAL) - }, - ) - } - - if (settingsUuidChoice.value == UUIDSetting.MANUAL.name) { -// Spacer(modifier = modifier.height(LPadding)) - + if (settingsUuidChoice.value == UUIDSetting.MANUAL.name) { Row( modifier = modifier @@ -317,7 +225,7 @@ fun MobileIdAndSmartIdServicesComponent( modifier .semantics { testTagsAsResourceId = true - }.testTag("mobileIdAndSmartIdServicesComponentRemoveIconButton"), + }.testTag("mobileIdAndSmartIdServicesRemoveIconButton"), imageVector = ImageVector.vectorResource(R.drawable.ic_icon_remove), contentDescription = "$clearButtonText $buttonName", ) @@ -325,7 +233,6 @@ fun MobileIdAndSmartIdServicesComponent( } } } - } } InvisibleElement(modifier = modifier) diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/settings/advanced/signingservices/TimestampServicesComponent.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/settings/advanced/signingservices/TimestampServicesComponent.kt index 383455809..bf90ca68c 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/settings/advanced/signingservices/TimestampServicesComponent.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/settings/advanced/signingservices/TimestampServicesComponent.kt @@ -23,23 +23,17 @@ package ee.ria.DigiDoc.ui.component.settings.advanced.signingservices import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.Row 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 import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.RadioButton import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -47,17 +41,12 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope 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.focus.FocusRequester -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription @@ -71,16 +60,13 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.navigation.NavHostController import ee.ria.DigiDoc.R import ee.ria.DigiDoc.domain.model.settings.TSASetting +import ee.ria.DigiDoc.ui.component.settings.shared.SettingsRadioCard import ee.ria.DigiDoc.ui.component.shared.InvisibleElement import ee.ria.DigiDoc.ui.component.shared.PrimaryTextField import ee.ria.DigiDoc.ui.component.support.textFieldValueSaver import ee.ria.DigiDoc.ui.theme.Dimensions.LPadding import ee.ria.DigiDoc.ui.theme.Dimensions.SPadding -import ee.ria.DigiDoc.ui.theme.Dimensions.XSBorder -import ee.ria.DigiDoc.ui.theme.Dimensions.XSPadding -import ee.ria.DigiDoc.ui.theme.buttonRoundedCornerShape import ee.ria.DigiDoc.utils.Route -import ee.ria.DigiDoc.utils.extensions.notAccessible import ee.ria.DigiDoc.viewmodel.shared.SharedCertificateViewModel import ee.ria.DigiDoc.viewmodel.shared.SharedSettingsViewModel import kotlinx.coroutines.Dispatchers.IO @@ -97,9 +83,7 @@ fun TimestampServicesComponent( navController: NavHostController, ) { val context = LocalContext.current - val focusManager = LocalFocusManager.current val scope = rememberCoroutineScope() - val focusRequester = remember { FocusRequester() } val configuration = sharedSettingsViewModel.updatedConfiguration.value @@ -108,7 +92,7 @@ fun TimestampServicesComponent( val setSettingsTsaUrl = sharedSettingsViewModel.dataStore::setSettingsTSAUrl val setTsaSetting = sharedSettingsViewModel.dataStore::setTsaSetting val defaultTsaServiceUrl = getSettingsTsaUrl().ifEmpty { configuration?.tsaUrl } ?: "" - val settingsTsaServiceChoice = remember { mutableStateOf(getTsaSetting().name) } + val settingsTsaServiceChoice = rememberSaveable { mutableStateOf(getTsaSetting().name) } var settingsTsaServiceUrl by rememberSaveable(stateSaver = textFieldValueSaver) { mutableStateOf( TextFieldValue( @@ -156,7 +140,6 @@ fun TimestampServicesComponent( val useDefaultAccessText = stringResource(R.string.main_settings_siva_default_access_title) val useManualAccessText = stringResource(R.string.main_settings_siva_default_manual_access_title) - val clearButtonText = stringResource(R.string.clear_text) val buttonName = stringResource(id = R.string.button_name) // Reset TSA URL when the user navigates away from this screen and has set default choice @@ -179,111 +162,33 @@ fun TimestampServicesComponent( text = accessToTimeStampingServicesTitleText, style = MaterialTheme.typography.titleLarge, modifier = - modifier + Modifier .padding(bottom = SPadding) .semantics { heading() }, ) - Card( - modifier = - modifier - .fillMaxWidth() - .padding(top = XSPadding, bottom = SPadding), - shape = buttonRoundedCornerShape, - border = - BorderStroke( - width = XSBorder, - color = MaterialTheme.colorScheme.onSurface, - ), - colors = CardDefaults.cardColors(containerColor = Color.Transparent), - ) { - Row( - modifier = - modifier - .fillMaxWidth() - .padding(SPadding) - .clickable { - settingsTsaServiceChoice.value = TSASetting.DEFAULT.name - setTsaSetting(TSASetting.DEFAULT) - }, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = useDefaultAccessText, - modifier = - modifier - .weight(1f) - .notAccessible(), - ) - RadioButton( - modifier = - modifier - .semantics { - contentDescription = useDefaultAccessText - }, - selected = settingsTsaServiceChoice.value == TSASetting.DEFAULT.name, - onClick = { - settingsTsaServiceChoice.value = TSASetting.DEFAULT.name - setTsaSetting(TSASetting.DEFAULT) - }, - ) - } - } + SettingsRadioCard( + modifier = modifier, + label = useDefaultAccessText, + selected = settingsTsaServiceChoice.value == TSASetting.DEFAULT.name, + onClick = { + settingsTsaServiceChoice.value = TSASetting.DEFAULT.name + setTsaSetting(TSASetting.DEFAULT) + }, + ) - Card( - modifier = - modifier - .fillMaxWidth() - .padding(top = XSPadding, bottom = SPadding), - shape = buttonRoundedCornerShape, - border = - BorderStroke( - width = XSBorder, - color = MaterialTheme.colorScheme.onSurface, - ), - colors = CardDefaults.cardColors(containerColor = Color.Transparent), + SettingsRadioCard( + modifier = modifier, + label = useManualAccessText, + selected = settingsTsaServiceChoice.value == TSASetting.MANUAL.name, + onClick = { + settingsTsaServiceChoice.value = TSASetting.MANUAL.name + setTsaSetting(TSASetting.MANUAL) + }, ) { - Column( - modifier = - modifier - .padding(SPadding) - .fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Row( - modifier = - modifier - .clickable { - settingsTsaServiceChoice.value = TSASetting.MANUAL.name - setTsaSetting(TSASetting.MANUAL) - }, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = useManualAccessText, - style = MaterialTheme.typography.bodyLarge, - modifier = - modifier - .weight(1f) - .notAccessible(), - ) - RadioButton( - modifier = - modifier - .semantics { - contentDescription = useManualAccessText - }, - selected = settingsTsaServiceChoice.value == TSASetting.MANUAL.name, - onClick = { - settingsTsaServiceChoice.value = TSASetting.MANUAL.name - setTsaSetting(TSASetting.MANUAL) - }, - ) - } - - if (settingsTsaServiceChoice.value == TSASetting.MANUAL.name) { + if (settingsTsaServiceChoice.value == TSASetting.MANUAL.name) { PrimaryTextField( modifier = Modifier.padding(vertical = LPadding), value = settingsTsaServiceUrl, @@ -302,6 +207,8 @@ fun TimestampServicesComponent( removeIconTestTag = "timestampServicesRemoveIconButton", ) + Spacer(modifier = Modifier.height(SPadding)) + Text( modifier = modifier @@ -385,7 +292,6 @@ fun TimestampServicesComponent( } } } - } } InvisibleElement(modifier = modifier) diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/settings/shared/SettingsRadioCard.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/settings/shared/SettingsRadioCard.kt new file mode 100644 index 000000000..68b3e740c --- /dev/null +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/settings/shared/SettingsRadioCard.kt @@ -0,0 +1,127 @@ +/* + * 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.settings.shared + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import ee.ria.DigiDoc.ui.theme.Dimensions.SPadding +import ee.ria.DigiDoc.ui.theme.Dimensions.XSBorder +import ee.ria.DigiDoc.ui.theme.Dimensions.XSPadding +import ee.ria.DigiDoc.ui.theme.buttonRoundedCornerShape +import ee.ria.DigiDoc.utils.extensions.notAccessible + +/** + * A styled Card containing a radio button row used in settings screens. + * + * When [content] is null the card renders a simple full-width row (label + radio button). + * When [content] is provided the card renders a Column with the label row on top followed by + * the supplied composable. The content lambda is always invoked when non-null; callers are + * responsible for any conditional rendering inside it (e.g. `if (selected) { ... }`). + */ +@Composable +fun SettingsRadioCard( + modifier: Modifier = Modifier, + label: String, + selected: Boolean, + onClick: () -> Unit, + content: @Composable (ColumnScope.() -> Unit)? = null, +) { + Card( + modifier = + modifier + .fillMaxWidth() + .padding(top = XSPadding, bottom = SPadding), + shape = buttonRoundedCornerShape, + border = + BorderStroke( + width = XSBorder, + color = MaterialTheme.colorScheme.onSurface, + ), + colors = CardDefaults.cardColors(containerColor = Color.Transparent), + ) { + if (content != null) { + Column( + modifier = + Modifier + .padding(SPadding) + .fillMaxWidth(), + horizontalAlignment = Alignment.Start, + ) { + RadioButtonRow(label = label, selected = selected, onClick = onClick, withPadding = false) + content() + } + } else { + RadioButtonRow(label = label, selected = selected, onClick = onClick, withPadding = true) + } + } +} + +@Composable +private fun RadioButtonRow( + label: String, + selected: Boolean, + onClick: () -> Unit, + withPadding: Boolean, +) { + Row( + modifier = + Modifier + .fillMaxWidth() + .then(if (withPadding) Modifier.padding(SPadding) else Modifier) + .clickable(onClick = onClick), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = label, + style = MaterialTheme.typography.bodyLarge, + modifier = + Modifier + .weight(1f) + .notAccessible(), + ) + RadioButton( + modifier = + Modifier + .semantics { + contentDescription = label + }, + selected = selected, + onClick = onClick, + ) + } +} diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/ColoredSignedStatusText.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/ColoredSignedStatusText.kt index 7a8af40cc..70bc752d2 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/ColoredSignedStatusText.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/ColoredSignedStatusText.kt @@ -30,8 +30,10 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview +import ee.ria.DigiDoc.R import ee.ria.DigiDoc.libdigidoclib.domain.model.ValidatorInterface import ee.ria.DigiDoc.ui.component.shared.TagBadge import ee.ria.DigiDoc.ui.theme.Green_2_50 @@ -39,6 +41,8 @@ import ee.ria.DigiDoc.ui.theme.Green_2_700 import ee.ria.DigiDoc.ui.theme.RIADigiDocTheme import ee.ria.DigiDoc.ui.theme.Red50 import ee.ria.DigiDoc.ui.theme.Red800 +import ee.ria.DigiDoc.utilsLib.date.DateUtil +import java.util.Date @OptIn(ExperimentalLayoutApi::class) @Composable @@ -46,6 +50,8 @@ fun ColoredSignedStatusText( text: String, status: ValidatorInterface.Status, modifier: Modifier = Modifier, + validUntil: Date? = null, + isSignatureExtended: Boolean = false, ) { val parts = text.split(" (", limit = 2) @@ -66,28 +72,47 @@ fun ColoredSignedStatusText( FlowRow( modifier = modifier, ) { - TagBadge( - text = parts[0], - backgroundColor = tagBackgroundColor, - contentColor = tagContentColor, - modifier = - modifier - .alignByBaseline() - .focusable() - .testTag("signatureUpdateListSignatureStatus"), - ) + if (validUntil != null) { + val isExpired = validUntil.before(Date()) + val validUntilBackgroundColor = if (isExpired) Red50 else Green_2_50 + val validUntilContentColor = if (isExpired) Red800 else Green_2_700 + val formattedDate = DateUtil.dateFormat.format(validUntil) + TagBadge( + text = stringResource(R.string.signature_valid_until, formattedDate), + backgroundColor = validUntilBackgroundColor, + contentColor = validUntilContentColor, + modifier = + modifier + .alignByBaseline() + .focusable() + .testTag("signatureUpdateListValidUntil"), + ) + } - if (parts.size > 1) { - Text( - text = " (${parts[1]}", - color = additionalTextColor, + if (!isSignatureExtended || validUntil == null) { + TagBadge( + text = parts[0], + backgroundColor = tagBackgroundColor, + contentColor = tagContentColor, modifier = modifier .alignByBaseline() .focusable() - .testTag("signatureUpdateListSignatureStatusCaution"), - style = TextStyle(fontSize = MaterialTheme.typography.bodyMedium.fontSize), + .testTag("signatureUpdateListSignatureStatus"), ) + + if (parts.size > 1) { + Text( + text = " (${parts[1]}", + color = additionalTextColor, + modifier = + modifier + .alignByBaseline() + .focusable() + .testTag("signatureUpdateListSignatureStatusCaution"), + style = TextStyle(fontSize = MaterialTheme.typography.bodyMedium.fontSize), + ) + } } } } diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/SignatureComponent.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/SignatureComponent.kt index 45d0f0583..a09b3251c 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/SignatureComponent.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/SignatureComponent.kt @@ -234,6 +234,8 @@ fun SignatureComponent( .padding(vertical = SBorder) .focusable(false) .notAccessible(), + validUntil = if (!isTimestamped) signature.validUntil else null, + isSignatureExtended = !isTimestamped && signature.archiveTimeStampCertificateDer.isNotEmpty(), ) if (!signature.signerRoles.isEmpty()) { Text( 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 1d7ea0967..00012a572 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 @@ -93,6 +93,7 @@ import androidx.navigation.compose.rememberNavController import ee.ria.DigiDoc.R import ee.ria.DigiDoc.common.Constant.ASICS_MIMETYPE import ee.ria.DigiDoc.common.Constant.DDOC_MIMETYPE +import ee.ria.DigiDoc.common.exception.NoInternetConnectionException import ee.ria.DigiDoc.cryptolib.CryptoContainer import ee.ria.DigiDoc.domain.model.notifications.ContainerNotificationType import ee.ria.DigiDoc.libdigidoclib.SignedContainer @@ -137,11 +138,13 @@ import ee.ria.DigiDoc.utilsLib.extensions.isContainer import ee.ria.DigiDoc.utilsLib.extensions.isSignedPDF import ee.ria.DigiDoc.utilsLib.extensions.mimeType import ee.ria.DigiDoc.utilsLib.file.FileUtil.sanitizeString +import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.debugLog import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.errorLog import ee.ria.DigiDoc.viewmodel.EncryptViewModel import ee.ria.DigiDoc.viewmodel.SigningViewModel import ee.ria.DigiDoc.viewmodel.shared.SharedContainerViewModel import ee.ria.DigiDoc.viewmodel.shared.SharedMenuViewModel +import ee.ria.DigiDoc.viewmodel.shared.SharedSettingsViewModel import ee.ria.DigiDoc.viewmodel.shared.SharedSignatureViewModel import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.Main @@ -151,6 +154,8 @@ import kotlinx.coroutines.withContext import org.apache.commons.io.FilenameUtils import java.io.File +private const val LOG_TAG = "SigningNavigation" + @OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class) @Composable fun SigningNavigation( @@ -161,6 +166,7 @@ fun SigningNavigation( sharedSignatureViewModel: SharedSignatureViewModel, signingViewModel: SigningViewModel = hiltViewModel(), encryptViewModel: EncryptViewModel = hiltViewModel(), + sharedSettingsViewModel: SharedSettingsViewModel = hiltViewModel(), ) { val signedContainer by sharedContainerViewModel.signedContainer.asFlow().collectAsState(null) val shouldResetContainer by signingViewModel.shouldResetSignedContainer.asFlow().collectAsState(false) @@ -293,6 +299,7 @@ fun SigningNavigation( val listState = rememberLazyListState() val showContainerCloseConfirmationDialog = rememberSaveable { mutableStateOf(false) } + val showExtendSignaturesConfirmDialog = rememberSaveable { mutableStateOf(false) } val showSivaDialog = rememberSaveable { mutableStateOf(false) } val nestedFile = rememberSaveable { mutableStateOf(null) } @@ -434,6 +441,12 @@ fun SigningNavigation( } } + val onExtendSignaturesActionClick: () -> Unit = { + debugLog(LOG_TAG, "Extend signatures button clicked, showing confirmation dialog") + showContainerBottomSheet.value = false + showExtendSignaturesConfirmDialog.value = true + } + var isSaved by remember { mutableStateOf(false) } val selectedSignedContainerTabIndex = rememberSaveable { mutableIntStateOf(0) } @@ -499,6 +512,7 @@ fun SigningNavigation( sharedContainerViewModel.signedMidStatus.asFlow().collect { status -> status?.let { if (status == MobileCreateSignatureProcessStatus.OK) { + extendToLTA(sharedSettingsViewModel, signedContainer) { showLoadingScreen.value = it } signatures = signedContainer?.getSignatures() ?: emptyList() withContext(Main) { selectedSignedContainerTabIndex.intValue = 1 @@ -516,6 +530,7 @@ fun SigningNavigation( sharedContainerViewModel.signedSidStatus.asFlow().collect { status -> status?.let { if (status == SessionStatusResponseProcessStatus.OK) { + extendToLTA(sharedSettingsViewModel, signedContainer) { showLoadingScreen.value = it } signatures = signedContainer?.getSignatures() ?: emptyList() withContext(Main) { signatureAddedSuccess.value = true @@ -533,6 +548,7 @@ fun SigningNavigation( sharedContainerViewModel.signedNFCStatus.asFlow().collect { status -> status?.let { if (status == true) { + extendToLTA(sharedSettingsViewModel, signedContainer) { showLoadingScreen.value = it } signatures = signedContainer?.getSignatures() ?: emptyList() withContext(Main) { signatureAddedSuccess.value = true @@ -550,6 +566,7 @@ fun SigningNavigation( sharedContainerViewModel.signedIDCardStatus.asFlow().collect { status -> status?.let { if (status == true) { + extendToLTA(sharedSettingsViewModel, signedContainer) { showLoadingScreen.value = it } signatures = signedContainer?.getSignatures() ?: emptyList() withContext(Main) { signatureAddedSuccess.value = true @@ -1237,8 +1254,14 @@ fun SigningNavigation( signedContainer, isNestedContainer, ), + isExtendSignaturesButtonShown = + signingViewModel.isExtendSignaturesButtonShown( + signedContainer, + isNestedContainer, + ), signedContainer = signedContainer, onEncryptClick = onEncryptActionClick, + onExtendSignaturesClick = onExtendSignaturesActionClick, saveFileLauncher = saveFileLauncher, saveFile = ::saveFile, ) @@ -1269,15 +1292,62 @@ fun SigningNavigation( signingViewModel = signingViewModel, navController = navController, onEncryptClick = onEncryptActionClick, - onExtendSignatureClick = { - // TODO: Implement extend signature click - }, ) if (showLoadingScreen.value) { LoadingScreen(modifier = modifier) } + if (showExtendSignaturesConfirmDialog.value) { + MessageDialog( + modifier = modifier, + title = stringResource(R.string.extend_signatures), + message = stringResource(R.string.extend_signatures_confirm_message), + showIcons = false, + dismissButtonText = stringResource(R.string.cancel_button), + confirmButtonText = stringResource(R.string.extend_button), + dismissButtonContentDescription = stringResource(R.string.cancel_button), + confirmButtonContentDescription = stringResource(R.string.extend_signatures), + onDismissRequest = { + showExtendSignaturesConfirmDialog.value = false + }, + onDismissButton = { + showExtendSignaturesConfirmDialog.value = false + }, + onConfirmButton = { + debugLog(LOG_TAG, "User confirmed signature extension") + showExtendSignaturesConfirmDialog.value = false + showLoadingScreen.value = true + scope.launch(IO) { + try { + debugLog(LOG_TAG, "Starting signature extension for container: ${signedContainer?.getName()}") + signedContainer?.extendSignatures() + debugLog(LOG_TAG, "Signature extension completed, reloading signatures") + val updatedSignatures = signedContainer?.getSignatures() ?: emptyList() + debugLog(LOG_TAG, "Reloaded ${updatedSignatures.size} signature(s) after extension") + withContext(Main) { + signatures = updatedSignatures + showLoadingScreen.value = false + } + showMessage(context, R.string.extend_signatures_success) + } catch (e: NoInternetConnectionException) { + errorLog(LOG_TAG, "No internet connection while extending signatures", e) + withContext(Main) { + showLoadingScreen.value = false + } + showMessage(context, R.string.no_internet_connection) + } catch (e: Exception) { + errorLog(LOG_TAG, "Failed to extend signatures for container: ${signedContainer?.getName()}", e) + withContext(Main) { + showLoadingScreen.value = false + } + showMessage(context, R.string.extend_signatures_error) + } + } + }, + ) + } + if (showContainerCloseConfirmationDialog.value) { MessageDialog( modifier = modifier, @@ -1362,6 +1432,9 @@ private fun handleBackButtonClick( } } } else { + sharedContainerViewModel.resetSignedContainer() + sharedContainerViewModel.resetCryptoContainer() + sharedContainerViewModel.resetContainerNotifications() sharedContainerViewModel.clearContainers() signingViewModel.handleBackButton() navController.navigateUp() @@ -1391,6 +1464,24 @@ private fun saveFile( } } +private suspend fun extendToLTA( + sharedSettingsViewModel: SharedSettingsViewModel, + signedContainer: SignedContainer?, + setLoading: (Boolean) -> Unit, +) { + if (!sharedSettingsViewModel.dataStore.getSettingsDefaultLTA()) return + if (signedContainer?.isDdoc() == true) return + withContext(Main) { setLoading(true) } + try { + withContext(IO) { signedContainer?.extendSignature() } + debugLog(LOG_TAG, "Auto LTA extension completed for: ${signedContainer?.getName()}") + } catch (e: Exception) { + errorLog(LOG_TAG, "Auto LTA extension failed for: ${signedContainer?.getName()}", e) + } finally { + withContext(Main) { setLoading(false) } + } +} + @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/signing/bottomsheet/ContainerBottomSheet.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/bottomsheet/ContainerBottomSheet.kt index 0f03a55da..e4afc86c7 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/bottomsheet/ContainerBottomSheet.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/bottomsheet/ContainerBottomSheet.kt @@ -40,8 +40,10 @@ fun ContainerBottomSheet( isEditContainerButtonShown: Boolean = true, openEditContainerNameDialog: MutableState, isEncryptButtonShown: Boolean = true, + isExtendSignaturesButtonShown: Boolean = true, signedContainer: SignedContainer?, onEncryptClick: () -> Unit, + onExtendSignaturesClick: () -> Unit, saveFileLauncher: ActivityResultLauncher, saveFile: (File, String?, ActivityResultLauncher) -> Unit, ) { @@ -91,6 +93,16 @@ fun ContainerBottomSheet( isExtraActionButtonShown = true, onClick = onEncryptClick, ), + BottomSheetButton( + showButton = isExtendSignaturesButtonShown, + icon = R.drawable.ic_m3_more_time_48dp_wght400, + text = stringResource(R.string.extend_signatures), + contentDescription = "${stringResource( + R.string.extend_signatures, + )} $buttonName", + isExtraActionButtonShown = false, + onClick = onExtendSignaturesClick, + ), ), ) } diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/bottomsheet/SignedContainerBottomSheet.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/bottomsheet/SignedContainerBottomSheet.kt index 2b653b620..a8f21e1cb 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/bottomsheet/SignedContainerBottomSheet.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/bottomsheet/SignedContainerBottomSheet.kt @@ -44,7 +44,6 @@ fun SignedContainerBottomSheet( signingViewModel: SigningViewModel, navController: NavHostController, onEncryptClick: () -> Unit, - onExtendSignatureClick: () -> Unit, ) { BottomSheet( modifier = modifier, @@ -77,14 +76,7 @@ fun SignedContainerBottomSheet( text = stringResource(R.string.main_menu_encrypt_container), isExtraActionButtonShown = true, onClick = onEncryptClick, - ), - BottomSheetButton( - showButton = !isNestedContainer, - icon = R.drawable.ic_m3_more_time_48dp_wght400, - text = stringResource(R.string.extend_signature_button), - isExtraActionButtonShown = true, - onClick = onExtendSignatureClick, - ), + ) ), ) } diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/certificate/ArchiveTimestampCertInfo.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/certificate/ArchiveTimestampCertInfo.kt new file mode 100644 index 000000000..569049942 --- /dev/null +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/certificate/ArchiveTimestampCertInfo.kt @@ -0,0 +1,27 @@ +/* + * 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") + +package ee.ria.DigiDoc.ui.component.signing.certificate + +data class ArchiveTimestampCertInfo( + val issuer: String, + val subject: String, +) diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/certificate/SignerDetailItem.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/certificate/SignerDetailItem.kt index 4c5a33018..eccd6bf1f 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/certificate/SignerDetailItem.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/certificate/SignerDetailItem.kt @@ -56,243 +56,284 @@ data class SignerDetailItem( ocspIssuerName: String?, tsSubjectName: String?, ocspSubjectName: String?, + archiveTimestampCertInfo: List, ): List = - listOf( - SignerDetailItem( - icon = 0, - label = R.string.signer_certificate_issuer_label, - value = signerIssuerName, - contentDescription = - if (value != null) { - "${stringResource( - id = R.string.signer_certificate_issuer_label, - )} $value" - } else { - "" - }, - testTag = "signersCertificateIssuer", - ), - SignerDetailItem( - label = R.string.signers_certificate_label, - value = - if (isTimestamp) { - NameUtil.formatName(signature.signedBy).uppercase() - } else { - NameUtil.formatName(signature.signedBy) - }, - certificate = signature.signingCertificateDer.x509Certificate(), - contentDescription = - if (value != null) { - "${stringResource( - id = R.string.signers_certificate_label, - )}, $value" - } else { - "" - }, - formatForAccessibility = true, - testTag = "signersCertificate", - ), - SignerDetailItem( - icon = R.drawable.ic_m3_open_in_new_48dp_wght400, - isLink = true, - label = R.string.signature_method_label, - value = signature.signatureMethod, - contentDescription = - if (value != null) { - "${stringResource( - id = R.string.signature_method_label, - )}, $value" - } else { - "" - }, - testTag = "signatureDetailMethod", - ), - SignerDetailItem( - icon = 0, - label = R.string.container_format_label, - value = (sharedContainerViewModel.currentContainer() as? SignedContainer)?.containerMimetype() ?: "", - contentDescription = - if (value != null) { - "${stringResource( - id = R.string.container_format_label, - )}, $value" - } else { - "" - }, - testTag = "containerDetailFormat", - ), - SignerDetailItem( - icon = 0, - label = R.string.signature_format_label, - value = signature.profile, - contentDescription = - if (value != null) { - "${stringResource( - id = R.string.signature_format_label, - )}, $value" - } else { - "" - }, - testTag = "signatureDetailFormat", - ), - SignerDetailItem( - icon = 0, - label = R.string.signed_file_count_label, - value = - sharedContainerViewModel.signedContainer.value - ?.rawContainer() - ?.dataFiles() - ?.size - .toString(), - contentDescription = - if (value != null) { - "${stringResource( - id = R.string.signed_file_count_label, - )}, $value" - } else { - "" - }, - testTag = "containerDetailSignedFileCount", - ), - SignerDetailItem( - icon = 0, - label = R.string.signature_timestamp_label, - value = DateUtil.getFormattedDateTime(signature.timeStampTime, false), - contentDescription = - if (value != null) { - "${stringResource( - id = R.string.signature_timestamp_label, - )}, $value" - } else { - "" - }, - testTag = "signatureDetailTimestamp", - ), - SignerDetailItem( - icon = 0, - label = R.string.signature_timestamp_utc_label, - value = DateUtil.getFormattedDateTime(signature.timeStampTime, true), - contentDescription = - if (value != null) { - "${stringResource( - id = R.string.signature_timestamp_utc_label, - )}, $value" - } else { - "" - }, - testTag = "signatureDetailTimestampUTC", - ), - SignerDetailItem( - icon = 0, - label = R.string.hash_value_of_signature_label, - value = signature.messageImprint.hexString(), - contentDescription = - if (value != null) { - "${stringResource( - id = R.string.hash_value_of_signature_label, - )}, $value" - } else { - "" - }, - testTag = "signatureDetailHashValue", - ), - SignerDetailItem( - icon = 0, - label = R.string.ts_certificate_issuer_label, - value = tsIssuerName, - contentDescription = - if (value != null) { - "${stringResource( - id = R.string.ts_certificate_issuer_label, - )}, $value" - } else { - "" - }, - testTag = "signatureDetailTimestampCertificateIssuer", - ), - SignerDetailItem( - label = R.string.ts_certificate_label, - value = tsSubjectName, - certificate = signature.timeStampCertificateDer.x509Certificate(), - contentDescription = - if (value != null) { - "${stringResource( - id = R.string.ts_certificate_label, - )}, $value" - } else { - "" - }, - testTag = "signatureDetailTimestampCertificate", - ), - SignerDetailItem( - icon = 0, - label = R.string.ocsp_certificate_issuer_label, - value = ocspIssuerName, - contentDescription = - if (value != null) { - "${stringResource( - id = R.string.ocsp_certificate_issuer_label, - )}, $value" - } else { - "" - }, - testTag = "signatureDetailOCSPCertificateIssuer", - ), - SignerDetailItem( - label = R.string.ocsp_certificate_label, - value = ocspSubjectName, - certificate = signature.ocspCertificateDer.x509Certificate(), - contentDescription = - if (value != null) { - "${stringResource( - id = R.string.ocsp_certificate_label, - )}, $value" - } else { - "" - }, - testTag = "signatureDetailOCSPCertificate", - ), - SignerDetailItem( - icon = 0, - label = R.string.ocsp_time_label, - value = DateUtil.getFormattedDateTime(signature.ocspProducedAt, false), - contentDescription = - if (value != null) { - "${stringResource( - id = R.string.ocsp_time_label, - )}, $value" - } else { - "" - }, - testTag = "signatureDetailOCSPTime", - ), - SignerDetailItem( - icon = 0, - label = R.string.ocsp_time_utc_label, - value = DateUtil.getFormattedDateTime(signature.ocspProducedAt, true), - contentDescription = - if (value != null) { - "${stringResource( - id = R.string.ocsp_time_utc_label, - )}, $value" - } else { - "" - }, - testTag = "signatureDetailOCSPTimeUTC", - ), - SignerDetailItem( - icon = 0, - label = R.string.signers_mobile_time_label, - value = DateUtil.getFormattedDateTime(signature.claimedSigningTime, true), - contentDescription = - if (value != null) { - "${stringResource( - id = R.string.signers_mobile_time_label, - )}, $value" - } else { - "" - }, - testTag = "signatureDetailSignersMobileTimeUTC", - ), - ) + buildList { + addAll(listOf( + SignerDetailItem( + icon = 0, + label = R.string.signer_certificate_issuer_label, + value = signerIssuerName, + contentDescription = + if (value != null) { + "${stringResource( + id = R.string.signer_certificate_issuer_label, + )} $value" + } else { + "" + }, + testTag = "signersCertificateIssuer", + ), + SignerDetailItem( + label = R.string.signers_certificate_label, + value = + if (isTimestamp) { + NameUtil.formatName(signature.signedBy).uppercase() + } else { + NameUtil.formatName(signature.signedBy) + }, + certificate = signature.signingCertificateDer.x509Certificate(), + contentDescription = + if (value != null) { + "${stringResource( + id = R.string.signers_certificate_label, + )}, $value" + } else { + "" + }, + formatForAccessibility = true, + testTag = "signersCertificate", + ), + SignerDetailItem( + icon = R.drawable.ic_m3_open_in_new_48dp_wght400, + isLink = true, + label = R.string.signature_method_label, + value = signature.signatureMethod, + contentDescription = + if (value != null) { + "${stringResource( + id = R.string.signature_method_label, + )}, $value" + } else { + "" + }, + testTag = "signatureDetailMethod", + ), + SignerDetailItem( + icon = 0, + label = R.string.container_format_label, + value = (sharedContainerViewModel.currentContainer() as? SignedContainer)?.containerMimetype() ?: "", + contentDescription = + if (value != null) { + "${stringResource( + id = R.string.container_format_label, + )}, $value" + } else { + "" + }, + testTag = "containerDetailFormat", + ), + SignerDetailItem( + icon = 0, + label = R.string.signature_format_label, + value = signature.profile, + contentDescription = + if (value != null) { + "${stringResource( + id = R.string.signature_format_label, + )}, $value" + } else { + "" + }, + testTag = "signatureDetailFormat", + ), + SignerDetailItem( + icon = 0, + label = R.string.signed_file_count_label, + value = + sharedContainerViewModel.signedContainer.value + ?.rawContainer() + ?.dataFiles() + ?.size + .toString(), + contentDescription = + if (value != null) { + "${stringResource( + id = R.string.signed_file_count_label, + )}, $value" + } else { + "" + }, + testTag = "containerDetailSignedFileCount", + ), + SignerDetailItem( + icon = 0, + label = R.string.signature_timestamp_label, + value = DateUtil.getFormattedDateTime(signature.timeStampTime, false), + contentDescription = + if (value != null) { + "${stringResource( + id = R.string.signature_timestamp_label, + )}, $value" + } else { + "" + }, + testTag = "signatureDetailTimestamp", + ), + SignerDetailItem( + icon = 0, + label = R.string.signature_timestamp_utc_label, + value = DateUtil.getFormattedDateTime(signature.timeStampTime, true), + contentDescription = + if (value != null) { + "${stringResource( + id = R.string.signature_timestamp_utc_label, + )}, $value" + } else { + "" + }, + testTag = "signatureDetailTimestampUTC", + ), + SignerDetailItem( + icon = 0, + label = R.string.hash_value_of_signature_label, + value = signature.messageImprint.hexString(), + contentDescription = + if (value != null) { + "${stringResource( + id = R.string.hash_value_of_signature_label, + )}, $value" + } else { + "" + }, + testTag = "signatureDetailHashValue", + ), + SignerDetailItem( + icon = 0, + label = R.string.ts_certificate_issuer_label, + value = tsIssuerName, + contentDescription = + if (value != null) { + "${stringResource( + id = R.string.ts_certificate_issuer_label, + )}, $value" + } else { + "" + }, + testTag = "signatureDetailTimestampCertificateIssuer", + ), + SignerDetailItem( + label = R.string.ts_certificate_label, + value = tsSubjectName, + certificate = signature.timeStampCertificateDer.x509Certificate(), + contentDescription = + if (value != null) { + "${stringResource( + id = R.string.ts_certificate_label, + )}, $value" + } else { + "" + }, + testTag = "signatureDetailTimestampCertificate", + ), + SignerDetailItem( + icon = 0, + label = R.string.ocsp_certificate_issuer_label, + value = ocspIssuerName, + contentDescription = + if (value != null) { + "${stringResource( + id = R.string.ocsp_certificate_issuer_label, + )}, $value" + } else { + "" + }, + testTag = "signatureDetailOCSPCertificateIssuer", + ), + SignerDetailItem( + label = R.string.ocsp_certificate_label, + value = ocspSubjectName, + certificate = signature.ocspCertificateDer.x509Certificate(), + contentDescription = + if (value != null) { + "${stringResource( + id = R.string.ocsp_certificate_label, + )}, $value" + } else { + "" + }, + testTag = "signatureDetailOCSPCertificate", + ), + SignerDetailItem( + icon = 0, + label = R.string.ocsp_time_label, + value = DateUtil.getFormattedDateTime(signature.ocspProducedAt, false), + contentDescription = + if (value != null) { + "${stringResource( + id = R.string.ocsp_time_label, + )}, $value" + } else { + "" + }, + testTag = "signatureDetailOCSPTime", + ), + SignerDetailItem( + icon = 0, + label = R.string.ocsp_time_utc_label, + value = DateUtil.getFormattedDateTime(signature.ocspProducedAt, true), + contentDescription = + if (value != null) { + "${stringResource( + id = R.string.ocsp_time_utc_label, + )}, $value" + } else { + "" + }, + testTag = "signatureDetailOCSPTimeUTC", + ), + SignerDetailItem( + icon = 0, + label = R.string.signers_mobile_time_label, + value = DateUtil.getFormattedDateTime(signature.claimedSigningTime, true), + contentDescription = + if (value != null) { + "${stringResource( + id = R.string.signers_mobile_time_label, + )}, $value" + } else { + "" + }, + testTag = "signatureDetailSignersMobileTimeUTC", + ), + )) + signature.archiveTimeStamps.zip(archiveTimestampCertInfo).forEachIndexed { index, (ts, names) -> + add(SignerDetailItem( + icon = 0, + label = R.string.archive_timestamp_label, + value = DateUtil.getFormattedDateTime(ts.time, false), + contentDescription = + if (value != null) { + "${stringResource(id = R.string.archive_timestamp_label)}, $value" + } else { + "" + }, + testTag = "archiveTimestamp$index", + )) + add(SignerDetailItem( + icon = 0, + label = R.string.archive_ts_certificate_issuer_label, + value = names.issuer, + contentDescription = + if (value != null) { + "${stringResource(id = R.string.archive_ts_certificate_issuer_label)}, $value" + } else { + "" + }, + testTag = "archiveTsCertificateIssuer$index", + )) + add(SignerDetailItem( + label = R.string.archive_ts_certificate_label, + value = names.subject, + certificate = ts.certificate, + contentDescription = + if (value != null) { + "${stringResource(id = R.string.archive_ts_certificate_label)}, $value" + } else { + "" + }, + testTag = "archiveTsCertificate$index", + )) + } + } } diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/certificate/SignerDetails.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/certificate/SignerDetails.kt index 7de90e38b..9c16b3b38 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/certificate/SignerDetails.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/certificate/SignerDetails.kt @@ -50,6 +50,7 @@ fun SignerDetails( ocspIssuerName: String, tsSubjectName: String, ocspSubjectName: String, + archiveTimestampCertInfo: List, sharedContainerViewModel: SharedContainerViewModel, sharedCertificateViewModel: SharedCertificateViewModel, navController: NavController, @@ -73,6 +74,7 @@ fun SignerDetails( ocspIssuerName = ocspIssuerName, tsSubjectName = tsSubjectName, ocspSubjectName = ocspSubjectName, + archiveTimestampCertInfo = archiveTimestampCertInfo, sharedContainerViewModel = sharedContainerViewModel, ).forEach { navigationItem -> if (!navigationItem.value.isNullOrEmpty()) { diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/certificate/SignerDetailsView.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/certificate/SignerDetailsView.kt index c5482f037..6fd69ddae 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/certificate/SignerDetailsView.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/certificate/SignerDetailsView.kt @@ -154,6 +154,13 @@ fun SignerDetailsView( certificateDetailViewModel.getSubjectCommonName( signature?.timeStampCertificateDer?.x509Certificate(), ) + val archiveTimestampCertInfo = + (signature?.archiveTimeStamps ?: emptyList()).map { ts -> + ArchiveTimestampCertInfo( + issuer = certificateDetailViewModel.getIssuerCommonName(ts.certificate), + subject = certificateDetailViewModel.getSubjectCommonName(ts.certificate), + ) + } val ocspSubjectName = certificateDetailViewModel.getSubjectCommonName( signature?.ocspCertificateDer?.x509Certificate(), @@ -302,6 +309,8 @@ fun SignerDetailsView( .padding(vertical = SBorder) .focusable(false) .notAccessible(), + validUntil = if (!isTimestamp) signature.validUntil else null, + isSignatureExtended = !isTimestamp && signature.archiveTimeStampCertificateDer.isNotEmpty(), ) } } @@ -364,6 +373,7 @@ fun SignerDetailsView( ocspIssuerName = ocspIssuerName, tsSubjectName = tsSubjectName, ocspSubjectName = ocspSubjectName, + archiveTimestampCertInfo = archiveTimestampCertInfo, sharedContainerViewModel = sharedContainerViewModel, sharedCertificateViewModel = sharedCertificateViewModel, navController = navController, 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..ce929944f 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/EncryptViewModel.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/EncryptViewModel.kt @@ -64,7 +64,7 @@ class EncryptViewModel val shouldResetCryptoContainer: LiveData = _shouldResetCryptoContainer fun handleBackButton() { - _shouldResetCryptoContainer.postValue(true) + _shouldResetCryptoContainer.value = true } fun isEncryptedContainer(cryptoContainer: CryptoContainer?): Boolean = cryptoContainer?.encrypted == true diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/SigningViewModel.kt b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/SigningViewModel.kt index 921485c20..7790d2cf3 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/SigningViewModel.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/SigningViewModel.kt @@ -72,7 +72,7 @@ class SigningViewModel val hasEmptyFiles: StateFlow = _hasEmptyFiles fun handleBackButton() { - _shouldResetSignedContainer.postValue(true) + _shouldResetSignedContainer.value = true } fun isExistingContainerNoSignatures(signedContainer: SignedContainer?): Boolean = @@ -136,6 +136,11 @@ class SigningViewModel isNestedContainer: Boolean, ): Boolean = signedContainer?.isSigned() == false && !isNestedContainer + fun isExtendSignaturesButtonShown( + signedContainer: SignedContainer?, + isNestedContainer: Boolean, + ): Boolean = signedContainer?.isSigned() == true && !isNestedContainer && !signedContainer.isDdoc() + fun isRoleEmpty(signature: SignatureInterface): Boolean = signature.signerRoles.isEmpty() && signature.city.isEmpty() && diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/shared/SharedSettingsViewModel.kt b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/shared/SharedSettingsViewModel.kt index 997bf362a..a7e43b7f9 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/shared/SharedSettingsViewModel.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/shared/SharedSettingsViewModel.kt @@ -203,6 +203,7 @@ class SharedSettingsViewModel dataStore.setSettingsUUID(DEFAULT_UUID_VALUE) dataStore.setSettingsTSAUrl(updatedConfiguration.value?.tsaUrl ?: "") dataStore.setSettingsAskRoleAndAddress(false) + dataStore.setSettingsDefaultLTA(false) dataStore.setIsTsaCertificateViewVisible(false) val certFile = FileUtil.getCertFile(context, dataStore.getTSACertName(), DIR_TSA_CERT) diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index fe8481bab..382fbd505 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -33,6 +33,12 @@ Allkirjastatud ümbrik Lisa veel faile Salvesta ümbrik + Pikenda allkirju + Pikenda + Kõik ümbriku allkirjad pikendatakse LTA formaati. + Allkirja(de) pikendamine õnnestus + Allkirja(de) pikendamine ebaõnnestus + Arhiivi ajatempel kehtib kuni: %1$s Salvesta Ümbrikus Veel valikuid @@ -341,7 +347,6 @@ Allkirjasta Edasi Lisa uus allkiri - Pikenda allkirja kehtivust Jaga Jaga ümbrikut JAH @@ -429,6 +434,9 @@ Kehtivuskinnituse aeg Kehtivuskinnituse aeg (UTC) Allkirjastaja telefoni kellaaeg (UTC) + Arhiivi ajatempel: + Arhiivi ajatempliteenuse sertifikaadi väljaandja: + Arhiivi ajatempliteenuse sertifikaat: Sertifikaadi detailid @@ -561,7 +569,10 @@ Krüpteerimisseaded Ajatempliteenus Mobiil-ID ja Smart-ID + LTA Küsi rolli ja aadressi igal allkirjastamisel + Ei kasuta vaikimisi LTAd allkirjastamisel + Kasuta vaikimisi LTAd allkirjastamisel Roll / resolutsioon Linn Maakond diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index 6007af785..1b66987eb 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -78,6 +78,7 @@ mainSettingsOpenAllFilesTypes mainSettingsAllowScreenshots mainSettingsAskRoleAndAddress + mainSettingsDefaultLTA mainSettingsTSACertView diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 802e6abc5..66fdc12a0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -33,6 +33,12 @@ Signed container Add more files Save container + Extend signatures + Extend + All signatures in the container will be extended to LTA format. + Signature(s) extended successfully + Failed to extend signature(s) + Archive timestamp valid until: %1$s Save Container has More options @@ -341,7 +347,6 @@ Sign Next Add new signature - Extend signature validity Share Share container YES @@ -429,6 +434,9 @@ OCSP time: OCSP time (UTC): Signer\'s mobile time: + Archive Timestamp: + Archive TS Certificate issuer: + Archive TS Certificate: Certificate details @@ -561,7 +569,10 @@ Encryption settings Time-Stamping service Mobile-ID and Smart-ID + LTA profile Ask role and address info on signing + Do not use LTA format by default when signing + Use LTA format by default when signing Role / resolution City State diff --git a/libdigidoc-lib/libs/libdigidocpp.jar b/libdigidoc-lib/libs/libdigidocpp.jar index a4771f02e..beee711b7 100644 Binary files a/libdigidoc-lib/libs/libdigidocpp.jar and b/libdigidoc-lib/libs/libdigidocpp.jar differ diff --git a/libdigidoc-lib/src/debug/jniLibs/arm64-v8a/libdigidoc_java.so b/libdigidoc-lib/src/debug/jniLibs/arm64-v8a/libdigidoc_java.so index a3cfbc1ba..aa27268de 100644 Binary files a/libdigidoc-lib/src/debug/jniLibs/arm64-v8a/libdigidoc_java.so and b/libdigidoc-lib/src/debug/jniLibs/arm64-v8a/libdigidoc_java.so differ diff --git a/libdigidoc-lib/src/debug/jniLibs/armeabi-v7a/libdigidoc_java.so b/libdigidoc-lib/src/debug/jniLibs/armeabi-v7a/libdigidoc_java.so index 5a7d455df..4fd1e3575 100644 Binary files a/libdigidoc-lib/src/debug/jniLibs/armeabi-v7a/libdigidoc_java.so and b/libdigidoc-lib/src/debug/jniLibs/armeabi-v7a/libdigidoc_java.so differ diff --git a/libdigidoc-lib/src/debug/jniLibs/x86_64/libdigidoc_java.so b/libdigidoc-lib/src/debug/jniLibs/x86_64/libdigidoc_java.so index e30e23bd3..034b6592f 100644 Binary files a/libdigidoc-lib/src/debug/jniLibs/x86_64/libdigidoc_java.so and b/libdigidoc-lib/src/debug/jniLibs/x86_64/libdigidoc_java.so differ diff --git a/libdigidoc-lib/src/main/jniLibs/arm64-v8a/libdigidoc_java.so b/libdigidoc-lib/src/main/jniLibs/arm64-v8a/libdigidoc_java.so index a3cfbc1ba..aa27268de 100644 Binary files a/libdigidoc-lib/src/main/jniLibs/arm64-v8a/libdigidoc_java.so and b/libdigidoc-lib/src/main/jniLibs/arm64-v8a/libdigidoc_java.so differ diff --git a/libdigidoc-lib/src/main/jniLibs/armeabi-v7a/libdigidoc_java.so b/libdigidoc-lib/src/main/jniLibs/armeabi-v7a/libdigidoc_java.so index 5a7d455df..4fd1e3575 100644 Binary files a/libdigidoc-lib/src/main/jniLibs/armeabi-v7a/libdigidoc_java.so and b/libdigidoc-lib/src/main/jniLibs/armeabi-v7a/libdigidoc_java.so differ diff --git a/libdigidoc-lib/src/main/jniLibs/x86_64/libdigidoc_java.so b/libdigidoc-lib/src/main/jniLibs/x86_64/libdigidoc_java.so index e30e23bd3..034b6592f 100644 Binary files a/libdigidoc-lib/src/main/jniLibs/x86_64/libdigidoc_java.so and b/libdigidoc-lib/src/main/jniLibs/x86_64/libdigidoc_java.so differ diff --git a/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/SignedContainer.kt b/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/SignedContainer.kt index b91068b85..72d56479d 100644 --- a/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/SignedContainer.kt +++ b/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/SignedContainer.kt @@ -169,6 +169,41 @@ class SignedContainer } } + @Throws(Exception::class) + fun extendSignature() { + extendSignatures(listOfNotNull(container?.signatures()?.lastOrNull())) + } + + @Throws(Exception::class) + fun extendSignatures() { + extendSignatures(container?.signatures() ?: emptyList()) + } + + private fun extendSignatures(signatures: List) { + if (signatures.isEmpty()) { + debugLog(LOG_TAG, "No signatures to extend") + return + } + val total = signatures.size + for ((index, signature) in signatures.withIndex()) { + try { + debugLog(LOG_TAG, "Extending signature ${index + 1}/$total: ${signature.id()}") + signature.extendSignatureProfile("time-stamp-archive") + debugLog(LOG_TAG, "Signature ${index + 1}/$total extended successfully") + } catch (e: Exception) { + errorLog(LOG_TAG, "Unable to extend signature ${signature.id()}", e) + handleContainerException(context, e) + } + } + try { + container?.save() + debugLog(LOG_TAG, "Container saved after signature extension") + } catch (e: Exception) { + errorLog(LOG_TAG, "Unable to save container after signature extension", e) + handleContainerException(context, e) + } + } + @Throws(Exception::class) fun getDataFile( dataFile: DataFileInterface, diff --git a/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/domain/model/ArchiveTimestamp.kt b/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/domain/model/ArchiveTimestamp.kt new file mode 100644 index 000000000..e185b06d8 --- /dev/null +++ b/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/domain/model/ArchiveTimestamp.kt @@ -0,0 +1,30 @@ +/* + * 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") + +package ee.ria.DigiDoc.libdigidoclib.domain.model + +import java.io.Serializable +import java.security.cert.X509Certificate + +data class ArchiveTimestamp( + val time: String, + val certificate: X509Certificate, +) : Serializable diff --git a/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/domain/model/SignatureInterface.kt b/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/domain/model/SignatureInterface.kt index d1d2c7f87..45d641491 100644 --- a/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/domain/model/SignatureInterface.kt +++ b/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/domain/model/SignatureInterface.kt @@ -22,6 +22,7 @@ package ee.ria.DigiDoc.libdigidoclib.domain.model import java.io.Serializable +import java.util.Date interface SignatureInterface : Serializable { val id: String @@ -50,4 +51,6 @@ interface SignatureInterface : Serializable { val archiveTimeStampCertificateDer: ByteArray val validator: ValidatorInterface + val validUntil: Date? get() = null + val archiveTimeStamps: List get() = emptyList() } diff --git a/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/domain/model/SignatureWrapper.kt b/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/domain/model/SignatureWrapper.kt index 4a30593b7..b17b2ccda 100644 --- a/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/domain/model/SignatureWrapper.kt +++ b/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/domain/model/SignatureWrapper.kt @@ -28,6 +28,7 @@ import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.errorLog import ee.ria.libdigidocpp.Signature import java.io.IOException import java.io.Serializable +import java.util.Date class SignatureWrapper( signature: Signature, @@ -94,6 +95,23 @@ class SignatureWrapper( override val validator: ValidatorInterface = ValidatorWrapper(Signature.Validator(signature)) + override val archiveTimeStamps: List = + try { + signature.ArchiveTimeStamps().mapNotNull { tsaInfo -> + try { + ArchiveTimestamp(time = tsaInfo.time, certificate = tsaInfo.cert) + } catch (e: Exception) { + errorLog(logTag, "Unable to parse archive timestamp entry", e) + null + } + } + } catch (e: Exception) { + errorLog(logTag, "Unable to get archive timestamps", e) + emptyList() + } + + override val validUntil: Date? = archiveTimeStamps.lastOrNull()?.certificate?.notAfter + private fun signatureName(signature: Signature): String { var commonName: String? try { diff --git a/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/init/Initialization.kt b/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/init/Initialization.kt index d1b645168..ae4da5d4a 100644 --- a/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/init/Initialization.kt +++ b/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/init/Initialization.kt @@ -128,7 +128,16 @@ class Initialization context, isLoggingEnabled, ) - digidoc.initializeLib(UserAgentUtil.getUserAgent(context), path) + + try { + digidoc.initializeLib(UserAgentUtil.getUserAgent(context), path, "") + } catch (e: RuntimeException) { + if (e.message?.startsWith("TSL") == true) { + errorLog(libdigidocInitLogTag, "Unable to initialize TSL: ${e.message}") + } else { + throw e + } + } isInitialized = true } diff --git a/libdigidoc-lib/src/main/res/raw/schema.zip b/libdigidoc-lib/src/main/res/raw/schema.zip index 1df7b7fc2..e1538aea1 100644 Binary files a/libdigidoc-lib/src/main/res/raw/schema.zip and b/libdigidoc-lib/src/main/res/raw/schema.zip differ