From e45d684cc5fcf22662313b9df3271102bcba2866 Mon Sep 17 00:00:00 2001 From: Marten Rebane Date: Thu, 5 Nov 2026 15:19:19 +0200 Subject: [PATCH] Fix cancelling and rotating during Smart-ID and Mobile-ID signing --- .../fragment/screen/SignatureInputScreen.kt | 2 - .../ui/component/signing/MobileIdView.kt | 30 +++-------- .../ui/component/signing/SmartIdView.kt | 32 +++--------- .../DigiDoc/viewmodel/MobileIdViewModel.kt | 7 --- .../ria/DigiDoc/viewmodel/SmartIdViewModel.kt | 7 --- .../mobileId/MobileSignServiceImplTest.kt | 48 +++++++++++++++++ .../ria/DigiDoc/mobileId/MobileSignService.kt | 4 ++ .../smartId/SmartSignServiceImplTest.kt | 51 +++++++++++++++++++ .../ria/DigiDoc/smartId/SmartSignService.kt | 4 ++ 9 files changed, 120 insertions(+), 65 deletions(-) diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/SignatureInputScreen.kt b/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/SignatureInputScreen.kt index 833e281e5..1b9dc776e 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/SignatureInputScreen.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/SignatureInputScreen.kt @@ -241,7 +241,6 @@ fun SignatureInputScreen( SigningMethod.MOBILE_ID -> MobileIdView( modifier = modifier, - activity = context, onError = { isSigning = false isAddingRoleAndAddress = false @@ -272,7 +271,6 @@ fun SignatureInputScreen( SigningMethod.SMART_ID -> SmartIdView( modifier = modifier, - activity = context, onError = { isSigning = false isAddingRoleAndAddress = false diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/MobileIdView.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/MobileIdView.kt index a5b32ae8e..ed8e9bbe4 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/MobileIdView.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/MobileIdView.kt @@ -5,11 +5,8 @@ package ee.ria.DigiDoc.ui.component.signing -import android.app.Activity -import android.content.pm.ActivityInfo import android.content.res.Configuration import androidx.activity.compose.BackHandler -import androidx.activity.compose.LocalActivity import androidx.compose.foundation.background import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Box @@ -28,7 +25,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -82,7 +78,6 @@ import kotlinx.coroutines.withContext @OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class) @Composable fun MobileIdView( - activity: Activity, modifier: Modifier = Modifier, isSigning: Boolean, onError: () -> Unit = {}, @@ -99,27 +94,12 @@ fun MobileIdView( val context = LocalContext.current val signedContainer by sharedContainerViewModel.signedContainer.collectAsState() val dialogError by mobileIdViewModel.dialogError.asFlow().collectAsState(0) - val isSigningInProgress by mobileIdViewModel.signingInProgress.collectAsState() val getSettingsAskRoleAndAddress = sharedSettingsViewModel.dataStore::getSettingsAskRoleAndAddress var shouldRememberMe by rememberSaveable { mutableStateOf(rememberMe) } val focusManager = LocalFocusManager.current - DisposableEffect(isSigningInProgress) { - if (isSigningInProgress) { - activity.requestedOrientation = - if (activity.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { - ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE - } else { - ActivityInfo.SCREEN_ORIENTATION_PORTRAIT - } - } - onDispose { - activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED - } - } - var countryCodeAndPhone by rememberSaveable(stateSaver = textFieldValueSaver) { mutableStateOf( TextFieldValue( @@ -184,6 +164,12 @@ fun MobileIdView( val phoneNumberFocusRequester = remember { FocusRequester() } val personalCodeFocusRequester = remember { FocusRequester() } + LaunchedEffect(signedContainer) { + cancelAction { + mobileIdViewModel.cancelMobileIdWorkRequest(signedContainer) + } + } + BackHandler { if (isSigning) { onError() @@ -384,9 +370,6 @@ fun MobileIdView( roleData = roleDataRequest, ) } - cancelAction { - mobileIdViewModel.cancelMobileIdWorkRequest(signedContainer) - } } } @@ -478,7 +461,6 @@ fun MobileIdViewPreview() { val sharedContainerViewModel: SharedContainerViewModel = hiltViewModel() RIADigiDocTheme { MobileIdView( - activity = LocalActivity.current as Activity, sharedSettingsViewModel = sharedSettingsViewModel, sharedContainerViewModel = sharedContainerViewModel, isSigning = false, diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/SmartIdView.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/SmartIdView.kt index 11421158c..db2ec8bb2 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/SmartIdView.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/SmartIdView.kt @@ -5,11 +5,8 @@ package ee.ria.DigiDoc.ui.component.signing -import android.app.Activity -import android.content.pm.ActivityInfo import android.content.res.Configuration import androidx.activity.compose.BackHandler -import androidx.activity.compose.LocalActivity import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.focusable @@ -33,7 +30,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -95,7 +91,6 @@ import kotlinx.coroutines.withContext @OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class) @Composable fun SmartIdView( - activity: Activity, modifier: Modifier = Modifier, isSigning: Boolean, onError: () -> Unit = {}, @@ -112,25 +107,10 @@ fun SmartIdView( val context = LocalContext.current val signedContainer by sharedContainerViewModel.signedContainer.collectAsState() val dialogError by smartIdViewModel.dialogError.asFlow().collectAsState(0) - val isSigningInProgress by smartIdViewModel.signingInProgress.collectAsState() val getSettingsAskRoleAndAddress = sharedSettingsViewModel.dataStore::getSettingsAskRoleAndAddress val focusManager = LocalFocusManager.current - DisposableEffect(isSigningInProgress) { - if (isSigningInProgress) { - activity.requestedOrientation = - if (activity.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { - ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE - } else { - ActivityInfo.SCREEN_ORIENTATION_PORTRAIT - } - } - onDispose { - activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED - } - } - val countryOptions = stringArrayResource(id = R.array.smart_id_country) var selectedCountry by rememberSaveable { mutableIntStateOf(sharedSettingsViewModel.dataStore.getCountry()) } var countryString by remember { mutableStateOf(countryOptions[selectedCountry]) } @@ -180,6 +160,13 @@ fun SmartIdView( val smartIdChallengeNotificationId = Constant.SmartIdConstants.NOTIFICATION_PERMISSION_CODE + LaunchedEffect(signedContainer) { + cancelAction { + smartIdViewModel.cancelSmartIdWorkRequest(signedContainer) + smartIdViewModel.cancelNotification(smartIdChallengeNotificationId) + } + } + BackHandler { if (isSigning) { onError() @@ -387,10 +374,6 @@ fun SmartIdView( roleData = roleDataRequest, ) } - cancelAction { - smartIdViewModel.cancelSmartIdWorkRequest(signedContainer) - smartIdViewModel.cancelNotification(smartIdChallengeNotificationId) - } } } @@ -580,7 +563,6 @@ fun SmartIdViewPreview() { val sharedSettingsViewModel: SharedSettingsViewModel = hiltViewModel() RIADigiDocTheme { SmartIdView( - activity = LocalActivity.current as Activity, sharedSettingsViewModel = sharedSettingsViewModel, sharedContainerViewModel = sharedContainerViewModel, isSigning = false, diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/MobileIdViewModel.kt b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/MobileIdViewModel.kt index bb35887aa..17ef55198 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/MobileIdViewModel.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/MobileIdViewModel.kt @@ -38,8 +38,6 @@ import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Job import kotlinx.coroutines.NonCancellable -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.util.Locale @@ -68,9 +66,6 @@ class MobileIdViewModel private val _dialogError = MutableLiveData(0) val dialogError: LiveData = _dialogError - private val _signingInProgress = MutableStateFlow(false) - val signingInProgress: StateFlow = _signingInProgress - private var signingJob: Job? = null private val faults: ImmutableMap = @@ -293,7 +288,6 @@ class MobileIdViewModel signingJob = viewModelScope.launch(IO) { - _signingInProgress.value = true try { resetValues() val configurationProvider = configurationRepository.getConfiguration() @@ -351,7 +345,6 @@ class MobileIdViewModel mobileSignService.status.removeObserver(statusObserver) mobileSignService.response.removeObserver(responseObserver) } - _signingInProgress.value = false } } diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/SmartIdViewModel.kt b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/SmartIdViewModel.kt index 95a9b1ec3..18309d163 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/SmartIdViewModel.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/SmartIdViewModel.kt @@ -41,9 +41,7 @@ import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Job import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.util.Objects @@ -75,9 +73,6 @@ class SmartIdViewModel private val _requestNotificationPermission = MutableSharedFlow>() val requestNotificationPermission: SharedFlow> = _requestNotificationPermission - private val _signingInProgress = MutableStateFlow(false) - val signingInProgress: StateFlow = _signingInProgress - private var signingJob: Job? = null private val countries: ImmutableMap = @@ -315,7 +310,6 @@ class SmartIdViewModel signingJob = viewModelScope.launch(IO) { - _signingInProgress.value = true try { resetValues() val configurationProvider = configurationRepository.getConfiguration() @@ -370,7 +364,6 @@ class SmartIdViewModel smartSignService.status.removeObserver(statusObserver) smartSignService.response.removeObserver(responseObserver) } - _signingInProgress.value = false } } diff --git a/mobile-id-lib/src/androidTest/kotlin/ee/ria/DigiDoc/mobileId/MobileSignServiceImplTest.kt b/mobile-id-lib/src/androidTest/kotlin/ee/ria/DigiDoc/mobileId/MobileSignServiceImplTest.kt index 3f9925703..2d09c0706 100644 --- a/mobile-id-lib/src/androidTest/kotlin/ee/ria/DigiDoc/mobileId/MobileSignServiceImplTest.kt +++ b/mobile-id-lib/src/androidTest/kotlin/ee/ria/DigiDoc/mobileId/MobileSignServiceImplTest.kt @@ -32,11 +32,13 @@ import ee.ria.DigiDoc.network.proxy.ManualProxy import ee.ria.DigiDoc.network.proxy.ProxySetting import ee.ria.DigiDoc.utilsLib.signing.TrustManagerUtil import ee.ria.libdigidocpp.ExternalSigner +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.ResponseBody.Companion.toResponseBody import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull import org.junit.Before import org.junit.BeforeClass import org.junit.Rule @@ -2632,4 +2634,50 @@ class MobileSignServiceImplTest { verify(statusObserver, never()).onChanged(MobileCreateSignatureProcessStatus.GENERAL_ERROR) verify(containerWrapper, never()).finalizeSignature(any(), any(), any()) } + + @Test + fun mobileSignService_processMobileIdRequest_rethrowsCancellationException() = + runTest { + doReturn(midRestServiceClient).whenever(serviceGenerator).createService( + context = context, + sslContext = null, + midSignServiceUrl = request.url, + certBundle = certBundle, + trustManagers = trustManagers, + proxySetting = proxySetting, + manualProxySettings = manualProxy, + ) + + val call = mock(Call::class.java) + doReturn(call) + .whenever(midRestServiceClient) + .getCertificate( + body = getMobileCertificateRequest, + ) + + doThrow(CancellationException("Signing cancelled")) + .whenever(call) + .execute() + + var cancellation: CancellationException? = null + try { + mobileSignServiceImpl.processMobileIdRequest( + context = context, + signedContainer = signedContainer, + request = request, + roleDataRequest = null, + proxySetting = proxySetting, + manualProxySettings = manualProxy, + certificateBundle = certBundle, + accessTokenPath = "accessTokenPath", + accessTokenPass = "accessTokenPass", + ) + } catch (ce: CancellationException) { + cancellation = ce + } + + assertNotNull(cancellation) + verify(statusObserver, never()).onChanged(MobileCreateSignatureProcessStatus.GENERAL_ERROR) + verify(statusObserver, never()).onChanged(MobileCreateSignatureProcessStatus.TECHNICAL_ERROR) + } } diff --git a/mobile-id-lib/src/main/kotlin/ee/ria/DigiDoc/mobileId/MobileSignService.kt b/mobile-id-lib/src/main/kotlin/ee/ria/DigiDoc/mobileId/MobileSignService.kt index 43bf18a2c..b1b21a731 100644 --- a/mobile-id-lib/src/main/kotlin/ee/ria/DigiDoc/mobileId/MobileSignService.kt +++ b/mobile-id-lib/src/main/kotlin/ee/ria/DigiDoc/mobileId/MobileSignService.kt @@ -45,6 +45,7 @@ import ee.ria.DigiDoc.utilsLib.signing.UUIDUtil import ee.ria.DigiDoc.utilsLib.text.MessageUtil import ee.ria.libdigidocpp.Container import ee.ria.libdigidocpp.ExternalSigner +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.delay import retrofit2.Call import retrofit2.Response @@ -491,6 +492,9 @@ class MobileSignServiceImpl // If user has cancelled signing, do not show any message setStatus(MobileCreateSignatureProcessStatus.USER_CANCELLED) return + } catch (ce: CancellationException) { + errorLog(logTag, "Mobile-ID signing cancelled", ce) + throw ce } catch (e: Exception) { val message = e.message if (!message.isNullOrEmpty() && message.contains("Too Many Requests")) { diff --git a/smart-id-lib/src/androidTest/kotlin/ee/ria/DigiDoc/smartId/SmartSignServiceImplTest.kt b/smart-id-lib/src/androidTest/kotlin/ee/ria/DigiDoc/smartId/SmartSignServiceImplTest.kt index bf8446998..502c66e21 100644 --- a/smart-id-lib/src/androidTest/kotlin/ee/ria/DigiDoc/smartId/SmartSignServiceImplTest.kt +++ b/smart-id-lib/src/androidTest/kotlin/ee/ria/DigiDoc/smartId/SmartSignServiceImplTest.kt @@ -32,10 +32,12 @@ import ee.ria.DigiDoc.network.sid.dto.response.SmartSignatureResponse import ee.ria.DigiDoc.network.sid.rest.SIDRestServiceClient import ee.ria.DigiDoc.network.sid.rest.ServiceGenerator import ee.ria.libdigidocpp.ExternalSigner +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.ResponseBody.Companion.toResponseBody +import org.junit.Assert.assertNotNull import org.junit.Before import org.junit.BeforeClass import org.junit.Rule @@ -2093,6 +2095,55 @@ class SmartSignServiceImplTest { verify(statusObserver, never()).onChanged(SessionStatusResponseProcessStatus.GENERAL_ERROR) } + @Test + fun smartSignService_processSmartIdRequest_rethrowsCancellationException() = + runTest { + doReturn(sidRestServiceClient).whenever(serviceGenerator).createService( + context = context, + sidSignServiceUrl = request.url + "/", + certBundle = certBundle, + proxySetting = proxySetting, + manualProxySettings = manualProxy, + ) + + val call = mock(Call::class.java) + doReturn(call) + .whenever(sidRestServiceClient) + .getCertificateV2( + semanticsIdentifier = "PNOEE-60001019906", + body = getSmartCertificateRequest, + ) + + doReturn(call) + .whenever(call) + .clone() + + doThrow(CancellationException("Signing cancelled")) + .whenever(call) + .execute() + + var cancellation: CancellationException? = null + try { + smartSignServiceImpl.processSmartIdRequest( + context = context, + signedContainer = signedContainer, + request = request, + roleDataRequest = null, + proxySetting = proxySetting, + manualProxySettings = manualProxy, + certificateBundle = certBundle, + accessTokenPath = "accessTokenPath", + accessTokenPass = "accessTokenPass", + ) + } catch (ce: CancellationException) { + cancellation = ce + } + + assertNotNull(cancellation) + verify(statusObserver, never()).onChanged(SessionStatusResponseProcessStatus.GENERAL_ERROR) + verify(statusObserver, never()).onChanged(SessionStatusResponseProcessStatus.TECHNICAL_ERROR) + } + private fun sessionStatusCall( state: SessionStatusResponseProcessState, endResult: SessionStatusResponseProcessStatus?, diff --git a/smart-id-lib/src/main/kotlin/ee/ria/DigiDoc/smartId/SmartSignService.kt b/smart-id-lib/src/main/kotlin/ee/ria/DigiDoc/smartId/SmartSignService.kt index 32ec843ba..a7f851bd1 100644 --- a/smart-id-lib/src/main/kotlin/ee/ria/DigiDoc/smartId/SmartSignService.kt +++ b/smart-id-lib/src/main/kotlin/ee/ria/DigiDoc/smartId/SmartSignService.kt @@ -40,6 +40,7 @@ import ee.ria.DigiDoc.utilsLib.signing.CertificateUtil import ee.ria.DigiDoc.utilsLib.signing.UUIDUtil import ee.ria.DigiDoc.utilsLib.text.MessageUtil import ee.ria.libdigidocpp.ExternalSigner +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.delay import retrofit2.Call import java.io.IOException @@ -393,6 +394,9 @@ class SmartSignServiceImpl "Exception: ${e.stackTrace.contentToString()}", ) return + } catch (ce: CancellationException) { + errorLog(logTag, "Smart-ID signing cancelled", ce) + throw ce } catch (e: Exception) { val message = e.message errorLog(