From 1ac3c8d77117542eabeb8894bfa00313d4245797 Mon Sep 17 00:00:00 2001 From: Marten Rebane Date: Fri, 9 Oct 2026 15:44:36 +0300 Subject: [PATCH] Block app when version is marked unsupported --- .../kotlin/ee/ria/DigiDoc/MainActivity.kt | 24 ++++- .../fragment/UnsupportedVersionFragment.kt | 49 +++++++++ .../screen/UnsupportedVersionScreen.kt | 99 +++++++++++++++++++ .../ee/ria/DigiDoc/utils/VersionUtil.kt | 39 ++++++++ app/src/main/res/values-et/strings.xml | 2 + app/src/main/res/values/donottranslate.xml | 1 + app/src/main/res/values/strings.xml | 2 + .../ee/ria/DigiDoc/utils/VersionUtilTest.kt | 71 +++++++++++++ .../provider/ConfigurationProvider.kt | 1 + .../provider/ConfigurationProviderTest.kt | 35 +++++++ 10 files changed, 318 insertions(+), 5 deletions(-) create mode 100644 app/src/main/kotlin/ee/ria/DigiDoc/fragment/UnsupportedVersionFragment.kt create mode 100644 app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/UnsupportedVersionScreen.kt create mode 100644 app/src/main/kotlin/ee/ria/DigiDoc/utils/VersionUtil.kt create mode 100644 app/src/test/kotlin/ee/ria/DigiDoc/utils/VersionUtilTest.kt create mode 100644 config-lib/src/test/kotlin/ee/ria/DigiDoc/configuration/provider/ConfigurationProviderTest.kt diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/MainActivity.kt b/app/src/main/kotlin/ee/ria/DigiDoc/MainActivity.kt index 37623453d..b49c330cb 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/MainActivity.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/MainActivity.kt @@ -12,24 +12,29 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.compose.runtime.getValue import androidx.core.app.ActivityCompat import androidx.core.net.toUri import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceManager import com.google.firebase.Firebase import com.google.firebase.crashlytics.crashlytics import dagger.hilt.android.AndroidEntryPoint import ee.ria.DigiDoc.common.model.AppState +import ee.ria.DigiDoc.configuration.loader.ConfigurationLoader import ee.ria.DigiDoc.domain.model.theme.ThemeSetting import ee.ria.DigiDoc.domain.preferences.DataStore import ee.ria.DigiDoc.fragment.RootFragment +import ee.ria.DigiDoc.fragment.UnsupportedVersionFragment import ee.ria.DigiDoc.init.LibrarySetup import ee.ria.DigiDoc.manager.ActivityManager import ee.ria.DigiDoc.root.RootChecker import ee.ria.DigiDoc.ui.theme.RIADigiDocTheme +import ee.ria.DigiDoc.utils.VersionUtil import ee.ria.DigiDoc.utils.WebEidUriUtil import ee.ria.DigiDoc.utils.locale.LocaleUtil import ee.ria.DigiDoc.utils.locale.LocaleUtilImpl @@ -73,6 +78,9 @@ class MainActivity : @Inject lateinit var secureUtil: SecureUtil + @Inject + lateinit var configurationLoader: ConfigurationLoader + private val logTag = "MainActivity" private var isAppReady = false @@ -163,13 +171,19 @@ class MainActivity : isAppReady = true } + val appVersion = "${BuildConfig.VERSION_NAME}.${BuildConfig.VERSION_CODE}" setContent { + val configuration by configurationLoader.getConfigurationFlow().collectAsStateWithLifecycle() RIADigiDocTheme(darkTheme = useDarkMode) { - RIADigiDocAppScreen( - externalFileUris = externalFileUris, - webEidUri = webEidUri, - browserPackage = browserPackage, - ) + if (VersionUtil.isLowerThan(appVersion, configuration?.riaddUnsupported)) { + UnsupportedVersionFragment() + } else { + RIADigiDocAppScreen( + externalFileUris = externalFileUris, + webEidUri = webEidUri, + browserPackage = browserPackage, + ) + } } } } diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/fragment/UnsupportedVersionFragment.kt b/app/src/main/kotlin/ee/ria/DigiDoc/fragment/UnsupportedVersionFragment.kt new file mode 100644 index 000000000..aac08bca2 --- /dev/null +++ b/app/src/main/kotlin/ee/ria/DigiDoc/fragment/UnsupportedVersionFragment.kt @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: Estonian Information System Authority +// SPDX-License-Identifier: LGPL-2.1-or-later + +@file:Suppress("PackageName", "FunctionName") + +package ee.ria.DigiDoc.fragment + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId +import androidx.compose.ui.tooling.preview.Preview +import ee.ria.DigiDoc.fragment.screen.UnsupportedVersionScreen +import ee.ria.DigiDoc.ui.theme.RIADigiDocTheme + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun UnsupportedVersionFragment(modifier: Modifier = Modifier) { + Surface( + modifier = + modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .semantics { + testTagsAsResourceId = true + }.testTag("unsupportedVersionFragment"), + color = MaterialTheme.colorScheme.background, + ) { + UnsupportedVersionScreen( + modifier = modifier, + ) + } +} + +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +fun UnsupportedVersionFragmentPreview() { + RIADigiDocTheme { + UnsupportedVersionFragment() + } +} diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/UnsupportedVersionScreen.kt b/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/UnsupportedVersionScreen.kt new file mode 100644 index 000000000..7efcb9cb2 --- /dev/null +++ b/app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/UnsupportedVersionScreen.kt @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: Estonian Information System Authority +// SPDX-License-Identifier: LGPL-2.1-or-later + +@file:Suppress("PackageName", "FunctionName") + +package ee.ria.DigiDoc.fragment.screen + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId +import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextLinkStyles +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import ee.ria.DigiDoc.R +import ee.ria.DigiDoc.ui.component.shared.InvisibleElement +import ee.ria.DigiDoc.ui.theme.Dimensions.SPadding + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun UnsupportedVersionScreen(modifier: Modifier = Modifier) { + val linkText = stringResource(R.string.unsupported_version_link_text) + val message = stringResource(R.string.unsupported_version_message, linkText) + val url = stringResource(R.string.unsupported_version_url) + val linkColor = MaterialTheme.colorScheme.primary + + val annotatedMessage = + buildAnnotatedString { + append(message) + val linkStart = message.indexOf(linkText) + if (linkStart >= 0) { + addLink( + LinkAnnotation.Url( + url = url, + styles = + TextLinkStyles( + style = + SpanStyle( + color = linkColor, + textDecoration = TextDecoration.Underline, + ), + ), + ), + linkStart, + linkStart + linkText.length, + ) + } + } + + Scaffold( + modifier = + modifier + .semantics { + testTagsAsResourceId = true + }.testTag("unsupportedVersionScreen"), + ) { innerPadding -> + Column( + modifier = modifier.padding(innerPadding), + ) { + Box( + modifier = + modifier + .padding(SPadding) + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .testTag("scrollView"), + contentAlignment = Alignment.Center, + ) { + Text( + text = annotatedMessage, + style = MaterialTheme.typography.bodyLarge, + modifier = + modifier + .padding(vertical = SPadding) + .testTag("unsupportedVersionMessage"), + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurface, + ) + InvisibleElement(modifier = modifier) + } + } + } +} diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/utils/VersionUtil.kt b/app/src/main/kotlin/ee/ria/DigiDoc/utils/VersionUtil.kt new file mode 100644 index 000000000..59a4ed3b7 --- /dev/null +++ b/app/src/main/kotlin/ee/ria/DigiDoc/utils/VersionUtil.kt @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: Estonian Information System Authority +// SPDX-License-Identifier: LGPL-2.1-or-later + +@file:Suppress("PackageName") + +package ee.ria.DigiDoc.utils + +object VersionUtil { + fun isLowerThan( + version: String, + otherVersion: String?, + ): Boolean { + val segments = segments(version) + val otherSegments = segments(otherVersion.orEmpty()) + if (segments.isEmpty() || otherSegments.isEmpty()) { + return false + } + + val length = maxOf(segments.size, otherSegments.size) + for (index in 0 until length) { + val segment = segments.getOrElse(index) { 0 } + val otherSegment = otherSegments.getOrElse(index) { 0 } + if (segment != otherSegment) { + return segment < otherSegment + } + } + return false + } + + private fun segments(version: String): List = + version + .trim() + .split('.') + .asSequence() + .map { it.toIntOrNull() } + .takeWhile { it != null && it >= 0 } + .filterNotNull() + .toList() +} diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index 046192a98..25224155c 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -21,6 +21,8 @@ Jätka Seda rakendust ei saa kasutada turvamata (ingl rooted) seadmes. + Sinu nutiseadmes olev ID-tarkvara ei ole toetatud. RIA Digidoc kasutamiseks pead ID-tarkvara uuendama. Paigalda uus ID-tarkvara veebilehelt %1$s. + https://www.id.ee/artikkel/paigalda-id-tarkvara/ Konfiguratsiooni initsialiseerimine ebaõnnestus Midagi läks valesti, proovi uuesti diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index 6ac55c1a2..6eda5abe9 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -17,6 +17,7 @@ English Invisible element + www.id.ee libdigidocpp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9a81b9590..2600a5e3a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -21,6 +21,8 @@ Continue This app cannot be used in rooted device. + This version of ID-software on your computer is unsupported. RIA DigiDoc cannot be used until you update ID-software. Install new ID-software from %1$s. + https://www.id.ee/en/article/install-id-software/ Failed to initialize configuration Something went wrong, try again diff --git a/app/src/test/kotlin/ee/ria/DigiDoc/utils/VersionUtilTest.kt b/app/src/test/kotlin/ee/ria/DigiDoc/utils/VersionUtilTest.kt new file mode 100644 index 000000000..6473b3b8f --- /dev/null +++ b/app/src/test/kotlin/ee/ria/DigiDoc/utils/VersionUtilTest.kt @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: Estonian Information System Authority +// SPDX-License-Identifier: LGPL-2.1-or-later + +@file:Suppress("PackageName") + +package ee.ria.DigiDoc.utils + +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class VersionUtilTest { + @Test + fun versionUtil_isLowerThan_returnTrueWhenBuildNumberIsLower() { + assertTrue(VersionUtil.isLowerThan("3.2.0.100", "3.2.0.101")) + } + + @Test + fun versionUtil_isLowerThan_returnTrueWhenMinorIsLowerDespiteHigherBuildNumber() { + assertTrue(VersionUtil.isLowerThan("3.1.9.999", "3.2.0.1")) + } + + @Test + fun versionUtil_isLowerThan_comparesSegmentsNumericallyNotLexically() { + assertTrue(VersionUtil.isLowerThan("3.9.0.1", "3.10.0.1")) + assertFalse(VersionUtil.isLowerThan("3.10.0.1", "3.9.0.1")) + } + + @Test + fun versionUtil_isLowerThan_returnFalseWhenEqual() { + assertFalse(VersionUtil.isLowerThan("3.2.0.100", "3.2.0.100")) + } + + @Test + fun versionUtil_isLowerThan_returnFalseWhenHigher() { + assertFalse(VersionUtil.isLowerThan("3.2.1.1", "3.2.0.500")) + } + + @Test + fun versionUtil_isLowerThan_treatsMissingSegmentsAsZero() { + assertTrue(VersionUtil.isLowerThan("3.2", "3.2.0.1")) + assertFalse(VersionUtil.isLowerThan("3.2.0.0", "3.2")) + assertFalse(VersionUtil.isLowerThan("3.2.0.1", "3.2")) + } + + @Test + fun versionUtil_isLowerThan_returnFalseWhenOtherVersionIsNull() { + assertFalse(VersionUtil.isLowerThan("3.2.0.100", null)) + } + + @Test + fun versionUtil_isLowerThan_returnFalseWhenOtherVersionIsBlank() { + assertFalse(VersionUtil.isLowerThan("3.2.0.100", " ")) + } + + @Test + fun versionUtil_isLowerThan_returnFalseWhenOtherVersionIsNotNumeric() { + assertFalse(VersionUtil.isLowerThan("3.2.0.100", "latest")) + } + + @Test + fun versionUtil_isLowerThan_ignoresTrailingNonNumericSuffix() { + assertTrue(VersionUtil.isLowerThan("3.2.0.100", "3.3.0-beta")) + assertFalse(VersionUtil.isLowerThan("3.3.0.1", "3.3.0-beta")) + } + + @Test + fun versionUtil_isLowerThan_trimsWhitespace() { + assertTrue(VersionUtil.isLowerThan("3.2.0.100", " 3.2.0.101 ")) + } +} diff --git a/config-lib/src/main/kotlin/ee/ria/DigiDoc/configuration/provider/ConfigurationProvider.kt b/config-lib/src/main/kotlin/ee/ria/DigiDoc/configuration/provider/ConfigurationProvider.kt index 1d7e290e1..fb8622eea 100644 --- a/config-lib/src/main/kotlin/ee/ria/DigiDoc/configuration/provider/ConfigurationProvider.kt +++ b/config-lib/src/main/kotlin/ee/ria/DigiDoc/configuration/provider/ConfigurationProvider.kt @@ -34,6 +34,7 @@ data class ConfigurationProvider( @SerializedName("LDAP-CERTS") val ldapCerts: List = listOf(), var configurationLastUpdateCheckDate: Date?, var configurationUpdateDate: Date?, + @SerializedName("RIADD-UNSUPPORTED") val riaddUnsupported: String? = null, ) { data class MetaInf( @SerializedName("URL") val url: String, diff --git a/config-lib/src/test/kotlin/ee/ria/DigiDoc/configuration/provider/ConfigurationProviderTest.kt b/config-lib/src/test/kotlin/ee/ria/DigiDoc/configuration/provider/ConfigurationProviderTest.kt new file mode 100644 index 000000000..f2929c1ee --- /dev/null +++ b/config-lib/src/test/kotlin/ee/ria/DigiDoc/configuration/provider/ConfigurationProviderTest.kt @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: Estonian Information System Authority +// SPDX-License-Identifier: LGPL-2.1-or-later + +@file:Suppress("PackageName") + +package ee.ria.DigiDoc.configuration.provider + +import com.google.gson.Gson +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +class ConfigurationProviderTest { + @Test + fun configurationProvider_deserialize_readsRiaddUnsupported() { + val configuration = + Gson().fromJson( + """{"RIADD-UNSUPPORTED":"3.2.0.100","QDIGIDOC4-UNSUPPORTED":"4.8.0.0"}""", + ConfigurationProvider::class.java, + ) + + assertEquals("3.2.0.100", configuration.riaddUnsupported) + } + + @Test + fun configurationProvider_deserialize_riaddUnsupportedIsNullWhenMissing() { + val configuration = + Gson().fromJson( + """{"QDIGIDOC4-UNSUPPORTED":"4.8.0.0"}""", + ConfigurationProvider::class.java, + ) + + assertNull(configuration.riaddUnsupported) + } +}