From 08d54f409fb31d24ca0e069ef6ed3649ef41066c Mon Sep 17 00:00:00 2001 From: Jamy Bailly Date: Mon, 10 Nov 2025 16:59:32 +0100 Subject: [PATCH 01/11] feat: Add module AppVersionChecker --- AppVersionChecker/build.gradle.kts | 52 ++++++++++++++++++ .../data/api/ApiRepositoryStores.kt | 53 +++++++++++++++++++ .../data/api/ApiRoutesStores.kt | 44 +++++++++++++++ .../data/models/AppPublishedVersion.kt | 2 +- .../data/models/AppVersion.kt | 13 ++++- InAppUpdate/build.gradle.kts | 2 +- .../infomaniak/core/inappupdate/StoreUtils.kt | 25 --------- .../updatemanagers/InAppUpdateManager.kt | 7 ++- .../inappupdate/BaseInAppUpdateManager.kt | 18 ++++++- .../core/inappupdate/StoresUtils.kt | 26 --------- .../data/api/ApiRepositoryStores.kt | 30 ----------- .../data/api/ApiRoutesStores.kt | 34 ------------ .../infomaniak/core/inappupdate/StoreUtils.kt | 27 ---------- .../updatemanagers/InAppUpdateManager.kt | 26 +++++---- 14 files changed, 201 insertions(+), 158 deletions(-) create mode 100644 AppVersionChecker/build.gradle.kts create mode 100644 AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/api/ApiRepositoryStores.kt create mode 100644 AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/api/ApiRoutesStores.kt rename {InAppUpdate/src/main/kotlin/com/infomaniak/core/inappupdate/updaterequired => AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker}/data/models/AppPublishedVersion.kt (92%) rename {InAppUpdate/src/main/kotlin/com/infomaniak/core/inappupdate/updaterequired => AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker}/data/models/AppVersion.kt (90%) delete mode 100644 InAppUpdate/src/fdroid/kotlin/com/infomaniak/core/inappupdate/StoreUtils.kt delete mode 100644 InAppUpdate/src/main/kotlin/com/infomaniak/core/inappupdate/StoresUtils.kt delete mode 100644 InAppUpdate/src/main/kotlin/com/infomaniak/core/inappupdate/updaterequired/data/api/ApiRepositoryStores.kt delete mode 100644 InAppUpdate/src/main/kotlin/com/infomaniak/core/inappupdate/updaterequired/data/api/ApiRoutesStores.kt delete mode 100644 InAppUpdate/src/standard/kotlin/com/infomaniak/core/inappupdate/StoreUtils.kt diff --git a/AppVersionChecker/build.gradle.kts b/AppVersionChecker/build.gradle.kts new file mode 100644 index 000000000..97ee20738 --- /dev/null +++ b/AppVersionChecker/build.gradle.kts @@ -0,0 +1,52 @@ +/* + * Infomaniak SwissTransfer - Android + * Copyright (C) 2025-2025 Infomaniak Network SA + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +plugins { + id("com.android.library") + alias(core.plugins.kotlin.android) +} + +val coreCompileSdk: Int by rootProject.extra +val coreMinSdk: Int by rootProject.extra +val javaVersion: JavaVersion by rootProject.extra + +android { + namespace = "com.infomaniak.core.appversionchecker" + compileSdk = coreCompileSdk + + defaultConfig { + minSdk = coreMinSdk + } + + compileOptions { + sourceCompatibility = javaVersion + targetCompatibility = javaVersion + } + + kotlinOptions { + jvmTarget = javaVersion.toString() + } +} + +dependencies { + implementation(project(":Core")) + implementation(project(":Core:Network")) + implementation(project(":Core:Sentry")) + + implementation(core.kotlinx.serialization.json) + implementation(core.okhttp) +} diff --git a/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/api/ApiRepositoryStores.kt b/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/api/ApiRepositoryStores.kt new file mode 100644 index 000000000..c54cdc95e --- /dev/null +++ b/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/api/ApiRepositoryStores.kt @@ -0,0 +1,53 @@ +/* + * Infomaniak Core - Android + * Copyright (C) 2025 Infomaniak Network SA + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.infomaniak.core.appversionchecker.data.api + +import com.infomaniak.core.appversionchecker.data.models.AppVersion +import com.infomaniak.core.network.api.ApiController +import com.infomaniak.core.network.api.ApiController.ApiMethod +import com.infomaniak.core.network.models.ApiResponse +import okhttp3.OkHttpClient + +object ApiRepositoryStores { + suspend fun getAppVersion( + appName: String, + store: AppVersion.Store, + okHttpClient: OkHttpClient + ): ApiResponse { + return ApiController.callApi(ApiRoutesStores.appVersion(appName, store), ApiMethod.GET, okHttpClient = okHttpClient) + } + + /** + * Get app version with projection to have a lighter JSON + * + * @param appName: App package name + * @param store: Specify which store is check + * @param projectionFields: A List of fields to keep to build response + * @param channelFilter: A optional parameter to filter by distribution channel. + * @param okHttpClient + */ + suspend fun getAppVersion( + appName: String, + store: AppVersion.Store, + projectionFields: List, + channelFilter: AppVersion.VersionChannel, + okHttpClient: OkHttpClient + ): ApiResponse { + return ApiController.callApi(ApiRoutesStores.appVersion(appName, store, projectionFields, channelFilter), ApiMethod.GET, okHttpClient = okHttpClient) + } +} diff --git a/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/api/ApiRoutesStores.kt b/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/api/ApiRoutesStores.kt new file mode 100644 index 000000000..b0d86b8c8 --- /dev/null +++ b/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/api/ApiRoutesStores.kt @@ -0,0 +1,44 @@ +/* + * Infomaniak Core - Android + * Copyright (C) 2025 Infomaniak Network SA + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.infomaniak.core.appversionchecker.data.api + +import com.infomaniak.core.appversionchecker.data.models.AppVersion +import com.infomaniak.core.network.INFOMANIAK_API_V1 +import io.sentry.cache.PersistingScopeObserver.store + +object ApiRoutesStores { + fun appVersion(appName: String, store: AppVersion.Store): String { + val platform = AppVersion.Platform.ANDROID.apiValue + + return "${INFOMANIAK_API_V1}/app-information/versions/${store.apiValue}/$platform/$appName" + } + + fun appVersion( + appName: String, + store: AppVersion.Store, + projectionFields: List, + channelFilter: AppVersion.VersionChannel? + ): String { + val parameters = buildString { + projectionFields.takeIf { it.isNotEmpty() }?.let { append("?only=${projectionFields.joinToString(",") { it.value }}") } + channelFilter?.let { append("&filter_versions[]=$channelFilter") } + } + + return appVersion(appName, store) + parameters + } +} diff --git a/InAppUpdate/src/main/kotlin/com/infomaniak/core/inappupdate/updaterequired/data/models/AppPublishedVersion.kt b/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/models/AppPublishedVersion.kt similarity index 92% rename from InAppUpdate/src/main/kotlin/com/infomaniak/core/inappupdate/updaterequired/data/models/AppPublishedVersion.kt rename to AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/models/AppPublishedVersion.kt index db6962098..d8de4aa36 100644 --- a/InAppUpdate/src/main/kotlin/com/infomaniak/core/inappupdate/updaterequired/data/models/AppPublishedVersion.kt +++ b/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/models/AppPublishedVersion.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.infomaniak.core.inappupdate.updaterequired.data.models +package com.infomaniak.core.appversionchecker.data.models import kotlinx.serialization.Serializable diff --git a/InAppUpdate/src/main/kotlin/com/infomaniak/core/inappupdate/updaterequired/data/models/AppVersion.kt b/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/models/AppVersion.kt similarity index 90% rename from InAppUpdate/src/main/kotlin/com/infomaniak/core/inappupdate/updaterequired/data/models/AppVersion.kt rename to AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/models/AppVersion.kt index a55d18570..ddb376334 100644 --- a/InAppUpdate/src/main/kotlin/com/infomaniak/core/inappupdate/updaterequired/data/models/AppVersion.kt +++ b/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/models/AppVersion.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package com.infomaniak.core.inappupdate.updaterequired.data.models +package com.infomaniak.core.appversionchecker.data.models import io.sentry.Sentry import io.sentry.SentryLevel @@ -39,6 +39,17 @@ data class AppVersion( ANDROID("android") } + enum class ProjectionFields(val value: String) { + MinVersion("min_version"), + PublishedVersionsTag("published_versions.tag") + } + + enum class VersionChannel(val value: String) { + Production("production"), + Beta("beta"), + Internal("internal"), + } + fun mustRequireUpdate(currentVersion: String): Boolean = runCatching { val currentVersionNumbers = currentVersion.toVersionNumbers() diff --git a/InAppUpdate/build.gradle.kts b/InAppUpdate/build.gradle.kts index aa409bdfb..93cc5a4b4 100644 --- a/InAppUpdate/build.gradle.kts +++ b/InAppUpdate/build.gradle.kts @@ -50,6 +50,7 @@ android { dependencies { implementation(project(":Core")) + implementation(project(":Core:AppVersionChecker")) implementation(project(":Core:Compose:Margin")) implementation(project(":Core:Network")) implementation(project(":Core:Sentry")) @@ -65,7 +66,6 @@ dependencies { implementation(core.androidx.concurrent.futures.ktx) implementation(core.okhttp) - implementation(core.gson) // Compose implementation(platform(core.compose.bom)) diff --git a/InAppUpdate/src/fdroid/kotlin/com/infomaniak/core/inappupdate/StoreUtils.kt b/InAppUpdate/src/fdroid/kotlin/com/infomaniak/core/inappupdate/StoreUtils.kt deleted file mode 100644 index 46b5b25f0..000000000 --- a/InAppUpdate/src/fdroid/kotlin/com/infomaniak/core/inappupdate/StoreUtils.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Infomaniak Core - Android - * Copyright (C) 2025 Infomaniak Network SA - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package com.infomaniak.core.inappupdate - -import com.infomaniak.core.inappupdate.updaterequired.data.models.AppVersion.Store - -object StoreUtils : StoresUtils { - const val APP_UPDATE_TAG = "appUpdateFDroid" - override val REQUIRED_UPDATE_STORE = Store.FDROID -} diff --git a/InAppUpdate/src/fdroid/kotlin/com/infomaniak/core/inappupdate/updatemanagers/InAppUpdateManager.kt b/InAppUpdate/src/fdroid/kotlin/com/infomaniak/core/inappupdate/updatemanagers/InAppUpdateManager.kt index 7133ea0ed..54b6831a1 100644 --- a/InAppUpdate/src/fdroid/kotlin/com/infomaniak/core/inappupdate/updatemanagers/InAppUpdateManager.kt +++ b/InAppUpdate/src/fdroid/kotlin/com/infomaniak/core/inappupdate/updatemanagers/InAppUpdateManager.kt @@ -19,12 +19,12 @@ package com.infomaniak.core.inappupdate.updatemanagers import androidx.activity.ComponentActivity import androidx.lifecycle.lifecycleScope +import com.infomaniak.core.appversionchecker.data.models.AppVersion import com.infomaniak.core.network.NetworkConfiguration.appId import com.infomaniak.core.network.NetworkConfiguration.appVersionCode import com.infomaniak.core.inappupdate.FdroidApiTools import com.infomaniak.core.sentry.SentryLog import com.infomaniak.core.inappupdate.BaseInAppUpdateManager -import com.infomaniak.core.inappupdate.StoreUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -33,8 +33,11 @@ class InAppUpdateManager( private val activity: ComponentActivity, ) : BaseInAppUpdateManager(activity) { + override val store: AppVersion.Store = AppVersion.Store.PLAY_STORE + override val appUpdateTag: String = "inAppUpdate" + override fun checkUpdateIsAvailable() { - SentryLog.d(StoreUtils.APP_UPDATE_TAG, "Checking for update on FDroid") + SentryLog.d(appUpdateTag, "Checking for update on FDroid") activity.lifecycleScope.launch(Dispatchers.IO) { val lastVersionCode = FdroidApiTools().getLastRelease(appId) diff --git a/InAppUpdate/src/main/kotlin/com/infomaniak/core/inappupdate/BaseInAppUpdateManager.kt b/InAppUpdate/src/main/kotlin/com/infomaniak/core/inappupdate/BaseInAppUpdateManager.kt index b4e8ac239..c63d484eb 100644 --- a/InAppUpdate/src/main/kotlin/com/infomaniak/core/inappupdate/BaseInAppUpdateManager.kt +++ b/InAppUpdate/src/main/kotlin/com/infomaniak/core/inappupdate/BaseInAppUpdateManager.kt @@ -22,12 +22,13 @@ import androidx.datastore.preferences.core.Preferences import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope +import com.infomaniak.core.appversionchecker.data.api.ApiRepositoryStores +import com.infomaniak.core.appversionchecker.data.models.AppVersion import com.infomaniak.core.extensions.goToAppStore import com.infomaniak.core.inappupdate.AppUpdateSettingsRepository.Companion.APP_UPDATE_LAUNCHES_KEY import com.infomaniak.core.inappupdate.AppUpdateSettingsRepository.Companion.DEFAULT_APP_UPDATE_LAUNCHES import com.infomaniak.core.inappupdate.AppUpdateSettingsRepository.Companion.HAS_APP_UPDATE_DOWNLOADED_KEY import com.infomaniak.core.inappupdate.AppUpdateSettingsRepository.Companion.IS_USER_WANTING_UPDATES_KEY -import com.infomaniak.core.inappupdate.updaterequired.data.api.ApiRepositoryStores import com.infomaniak.core.network.NetworkConfiguration.appId import com.infomaniak.core.network.NetworkConfiguration.appVersionName import com.infomaniak.core.network.networking.HttpClient @@ -40,6 +41,9 @@ import kotlinx.coroutines.launch abstract class BaseInAppUpdateManager(private val activity: ComponentActivity) : DefaultLifecycleObserver { + abstract val store: AppVersion.Store + abstract val appUpdateTag: String + protected var onInAppUpdateUiChange: ((Boolean) -> Unit)? = null protected var onFDroidResult: ((Boolean) -> Unit)? = null @@ -51,7 +55,17 @@ abstract class BaseInAppUpdateManager(private val activity: ComponentActivity) : .flowOf(HAS_APP_UPDATE_DOWNLOADED_KEY).distinctUntilChanged() val isUpdateRequired = flow { - val apiResponse = ApiRepositoryStores.getAppVersion(appId, HttpClient.okHttpClient) + val projectionFields = listOf( + AppVersion.ProjectionFields.MinVersion, + AppVersion.ProjectionFields.PublishedVersionsTag + ) + val apiResponse = ApiRepositoryStores.getAppVersion( + appName = appId, + store = store, + projectionFields = projectionFields, + channelFilter = AppVersion.VersionChannel.Production, + okHttpClient = HttpClient.okHttpClient + ) emit(apiResponse.data?.mustRequireUpdate(appVersionName) == true) }.stateIn( diff --git a/InAppUpdate/src/main/kotlin/com/infomaniak/core/inappupdate/StoresUtils.kt b/InAppUpdate/src/main/kotlin/com/infomaniak/core/inappupdate/StoresUtils.kt deleted file mode 100644 index 504d3a3bd..000000000 --- a/InAppUpdate/src/main/kotlin/com/infomaniak/core/inappupdate/StoresUtils.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Infomaniak Core - Android - * Copyright (C) 2025 Infomaniak Network SA - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package com.infomaniak.core.inappupdate - -import com.infomaniak.core.inappupdate.updaterequired.data.models.AppVersion - -internal interface StoresUtils { - - @Suppress("PropertyName") - val REQUIRED_UPDATE_STORE: AppVersion.Store -} diff --git a/InAppUpdate/src/main/kotlin/com/infomaniak/core/inappupdate/updaterequired/data/api/ApiRepositoryStores.kt b/InAppUpdate/src/main/kotlin/com/infomaniak/core/inappupdate/updaterequired/data/api/ApiRepositoryStores.kt deleted file mode 100644 index 7f43a98fd..000000000 --- a/InAppUpdate/src/main/kotlin/com/infomaniak/core/inappupdate/updaterequired/data/api/ApiRepositoryStores.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Infomaniak Core - Android - * Copyright (C) 2025 Infomaniak Network SA - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package com.infomaniak.core.inappupdate.updaterequired.data.api - -import com.infomaniak.core.inappupdate.updaterequired.data.models.AppVersion -import com.infomaniak.core.network.api.ApiController -import com.infomaniak.core.network.api.ApiController.ApiMethod -import com.infomaniak.core.network.models.ApiResponse -import okhttp3.OkHttpClient - -object ApiRepositoryStores { - suspend fun getAppVersion(appName: String, okHttpClient: OkHttpClient): ApiResponse { - return ApiController.callApi(ApiRoutesStores.appVersion(appName), ApiMethod.GET, okHttpClient = okHttpClient) - } -} diff --git a/InAppUpdate/src/main/kotlin/com/infomaniak/core/inappupdate/updaterequired/data/api/ApiRoutesStores.kt b/InAppUpdate/src/main/kotlin/com/infomaniak/core/inappupdate/updaterequired/data/api/ApiRoutesStores.kt deleted file mode 100644 index 3e8d21591..000000000 --- a/InAppUpdate/src/main/kotlin/com/infomaniak/core/inappupdate/updaterequired/data/api/ApiRoutesStores.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Infomaniak Core - Android - * Copyright (C) 2025 Infomaniak Network SA - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package com.infomaniak.core.inappupdate.updaterequired.data.api - -import com.infomaniak.core.inappupdate.StoreUtils -import com.infomaniak.core.inappupdate.updaterequired.data.models.AppVersion.Platform -import com.infomaniak.core.network.INFOMANIAK_API_V1 - -object ApiRoutesStores { - - fun appVersion(appName: String): String { - val store = StoreUtils.REQUIRED_UPDATE_STORE.apiValue - val platform = Platform.ANDROID.apiValue - - val parameters = "?only=min_version,published_versions.tag&filter_versions[]=production" - - return "${INFOMANIAK_API_V1}/app-information/versions/$store/$platform/$appName$parameters" - } -} diff --git a/InAppUpdate/src/standard/kotlin/com/infomaniak/core/inappupdate/StoreUtils.kt b/InAppUpdate/src/standard/kotlin/com/infomaniak/core/inappupdate/StoreUtils.kt deleted file mode 100644 index 55882223b..000000000 --- a/InAppUpdate/src/standard/kotlin/com/infomaniak/core/inappupdate/StoreUtils.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Infomaniak Core - Android - * Copyright (C) 2025 Infomaniak Network SA - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package com.infomaniak.core.inappupdate - -import com.google.android.play.core.install.model.AppUpdateType -import com.infomaniak.core.inappupdate.updaterequired.data.models.AppVersion.Store - -object StoreUtils : StoresUtils { - const val APP_UPDATE_TAG = "inAppUpdate" - const val DEFAULT_UPDATE_TYPE = AppUpdateType.FLEXIBLE - override val REQUIRED_UPDATE_STORE = Store.PLAY_STORE -} diff --git a/InAppUpdate/src/standard/kotlin/com/infomaniak/core/inappupdate/updatemanagers/InAppUpdateManager.kt b/InAppUpdate/src/standard/kotlin/com/infomaniak/core/inappupdate/updatemanagers/InAppUpdateManager.kt index fe4317076..cf15de1b2 100644 --- a/InAppUpdate/src/standard/kotlin/com/infomaniak/core/inappupdate/updatemanagers/InAppUpdateManager.kt +++ b/InAppUpdate/src/standard/kotlin/com/infomaniak/core/inappupdate/updatemanagers/InAppUpdateManager.kt @@ -33,12 +33,12 @@ import com.google.android.play.core.install.model.AppUpdateType import com.google.android.play.core.install.model.InstallStatus import com.google.android.play.core.install.model.UpdateAvailability import com.google.android.play.core.ktx.installErrorCode +import com.infomaniak.core.appversionchecker.data.models.AppVersion import com.infomaniak.core.inappupdate.AppUpdateSettingsRepository.Companion.APP_UPDATE_LAUNCHES_KEY import com.infomaniak.core.inappupdate.AppUpdateSettingsRepository.Companion.DEFAULT_APP_UPDATE_LAUNCHES import com.infomaniak.core.inappupdate.AppUpdateSettingsRepository.Companion.HAS_APP_UPDATE_DOWNLOADED_KEY import com.infomaniak.core.inappupdate.AppUpdateSettingsRepository.Companion.IS_USER_WANTING_UPDATES_KEY import com.infomaniak.core.inappupdate.BaseInAppUpdateManager -import com.infomaniak.core.inappupdate.StoreUtils import com.infomaniak.core.sentry.SentryLog import io.sentry.Sentry import io.sentry.SentryLevel @@ -55,6 +55,9 @@ class InAppUpdateManager( private val activity: ComponentActivity, ) : BaseInAppUpdateManager(activity) { + override val store: AppVersion.Store = AppVersion.Store.PLAY_STORE + override val appUpdateTag: String = APP_UPDATE_TAG + private val appUpdateManager = AppUpdateManagerFactory.create(activity) // Result of in app update's bottomSheet user choice private val inAppUpdateResultLauncher: ActivityResultLauncher = activity.registerForActivityResult( @@ -74,25 +77,25 @@ class InAppUpdateManager( private var onInstallFailure: ((Exception) -> Unit)? = null private var onInstallSuccess: (() -> Unit)? = null - private var updateType: Int = StoreUtils.DEFAULT_UPDATE_TYPE + private var updateType: Int = DEFAULT_UPDATE_TYPE // Create a listener to track request state updates. private val installStateUpdatedListener by lazy { InstallStateUpdatedListener { state -> when (state.installStatus()) { InstallStatus.DOWNLOADED -> { - SentryLog.d(StoreUtils.APP_UPDATE_TAG, "OnUpdateDownloaded triggered by InstallStateUpdated listener") + SentryLog.d(appUpdateTag, "OnUpdateDownloaded triggered by InstallStateUpdated listener") if (updateType == AppUpdateType.FLEXIBLE) onUpdateDownloaded() } InstallStatus.FAILED -> { - SentryLog.d(StoreUtils.APP_UPDATE_TAG, "onInstallFailure triggered by InstallStateUpdated listener") + SentryLog.d(appUpdateTag, "onInstallFailure triggered by InstallStateUpdated listener") if (updateType == AppUpdateType.IMMEDIATE) { resetUpdateSettings() onInstallFailure?.invoke(InstallException(state.installErrorCode)) } } InstallStatus.INSTALLED -> { - SentryLog.d(StoreUtils.APP_UPDATE_TAG, "OnUpdateInstalled triggered by InstallStateUpdated listener") + SentryLog.d(appUpdateTag, "OnUpdateInstalled triggered by InstallStateUpdated listener") if (updateType == AppUpdateType.FLEXIBLE) onUpdateInstalled() unregisterAppUpdateListener() } @@ -138,13 +141,13 @@ class InAppUpdateManager( } override fun checkUpdateIsAvailable() { - SentryLog.d(StoreUtils.APP_UPDATE_TAG, "Checking for update on GPlay") + SentryLog.d(appUpdateTag, "Checking for update on GPlay") appUpdateManager.appUpdateInfo.addOnSuccessListener { appUpdateInfo -> - SentryLog.d(StoreUtils.APP_UPDATE_TAG, "checking success") + SentryLog.d(appUpdateTag, "checking success") if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE && appUpdateInfo.isUpdateTypeAllowed(updateType) ) { - SentryLog.d(StoreUtils.APP_UPDATE_TAG, "Update available on GPlay") + SentryLog.d(appUpdateTag, "Update available on GPlay") startUpdateFlow(appUpdateInfo) } } @@ -191,7 +194,7 @@ class InAppUpdateManager( registerListener(installStateUpdatedListener) appUpdateInfo.addOnSuccessListener { appUpdateInfo -> if (appUpdateInfo.installStatus() == InstallStatus.DOWNLOADED) { - SentryLog.d(StoreUtils.APP_UPDATE_TAG, "CheckStalledUpdate downloaded") + SentryLog.d(appUpdateTag, "CheckStalledUpdate downloaded") // If the update is downloaded but not installed, notify the user to complete the update. onUpdateDownloaded.invoke() } @@ -224,4 +227,9 @@ class InAppUpdateManager( } private class AppUpdateException(override val message: String?) : Exception() + + companion object { + const val APP_UPDATE_TAG = "inAppUpdate" + const val DEFAULT_UPDATE_TYPE = AppUpdateType.FLEXIBLE + } } From 827c1d74df4dd331cca677b2fac8219156fca455 Mon Sep 17 00:00:00 2001 From: Jamy Bailly Date: Wed, 12 Nov 2025 14:50:21 +0100 Subject: [PATCH 02/11] feat: Add all optional field --- AppVersionChecker/build.gradle.kts | 1 + .../data/api/ApiRepositoryStores.kt | 7 +++++- .../data/models/AppPublishedVersion.kt | 17 ++++++++++++- .../data/models/AppVersion.kt | 24 ++++++++++++++----- .../inappupdate/BaseInAppUpdateManager.kt | 1 + 5 files changed, 42 insertions(+), 8 deletions(-) diff --git a/AppVersionChecker/build.gradle.kts b/AppVersionChecker/build.gradle.kts index 97ee20738..406bfef20 100644 --- a/AppVersionChecker/build.gradle.kts +++ b/AppVersionChecker/build.gradle.kts @@ -18,6 +18,7 @@ plugins { id("com.android.library") alias(core.plugins.kotlin.android) + kotlin("plugin.serialization") } val coreCompileSdk: Int by rootProject.extra diff --git a/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/api/ApiRepositoryStores.kt b/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/api/ApiRepositoryStores.kt index c54cdc95e..717008202 100644 --- a/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/api/ApiRepositoryStores.kt +++ b/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/api/ApiRepositoryStores.kt @@ -48,6 +48,11 @@ object ApiRepositoryStores { channelFilter: AppVersion.VersionChannel, okHttpClient: OkHttpClient ): ApiResponse { - return ApiController.callApi(ApiRoutesStores.appVersion(appName, store, projectionFields, channelFilter), ApiMethod.GET, okHttpClient = okHttpClient) + return ApiController.callApi( + url = ApiRoutesStores.appVersion(appName, store, projectionFields, channelFilter), + method = ApiMethod.GET, + okHttpClient = okHttpClient, + useKotlinxSerialization = true + ) } } diff --git a/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/models/AppPublishedVersion.kt b/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/models/AppPublishedVersion.kt index d8de4aa36..5873c2466 100644 --- a/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/models/AppPublishedVersion.kt +++ b/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/models/AppPublishedVersion.kt @@ -17,7 +17,22 @@ */ package com.infomaniak.core.appversionchecker.data.models +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -data class AppPublishedVersion(var tag: String) +data class AppPublishedVersion( + val tag: String? = null, + @SerialName("tag_updated_at") + val tagUpdatedAt: String? = null, + @SerialName("version_changelog") + val versionChangelog: String? = null, + val type: String? = null, + @SerialName("build_version") + val buildVersion: String? = null, + @SerialName("build_min_os_version") + val buildMinOsVersion: String? = null, + @SerialName("download_link") + val downloadLink: String? = null, + val data: List? = null +) diff --git a/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/models/AppVersion.kt b/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/models/AppVersion.kt index ddb376334..1aab95f08 100644 --- a/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/models/AppVersion.kt +++ b/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/models/AppVersion.kt @@ -17,17 +17,24 @@ */ package com.infomaniak.core.appversionchecker.data.models -import io.sentry.Sentry -import io.sentry.SentryLevel +import com.infomaniak.core.sentry.SentryLog import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable data class AppVersion( + val id: Int? = null, + val name: String? = null, + val platform: String? = null, + val store: String? = null, + @SerialName("api_id") + val apiId: String? = null, @SerialName("min_version") - var minimalAcceptedVersion: String, + val minimalAcceptedVersion: String? = null, + @SerialName("next_version_rate") + val nextVersionRate: String? = null, @SerialName("published_versions") - var publishedVersions: List, + val publishedVersions: List, ) { enum class Store(val apiValue: String) { @@ -51,6 +58,10 @@ data class AppVersion( } fun mustRequireUpdate(currentVersion: String): Boolean = runCatching { + if (minimalAcceptedVersion == null) { + SentryLog.d(TAG, "min_version field is empty. Don't forget to use AppVersion.ProjectionFields.MinVersion") + return false + } val currentVersionNumbers = currentVersion.toVersionNumbers() val minimalAcceptedVersionNumbers = minimalAcceptedVersion.toVersionNumbers() @@ -58,8 +69,7 @@ data class AppVersion( return isMinimalVersionValid(minimalAcceptedVersionNumbers) && currentVersionNumbers.compareVersionTo(minimalAcceptedVersionNumbers) < 0 }.getOrElse { exception -> - Sentry.captureException(exception) { scope -> - scope.level = SentryLevel.ERROR + SentryLog.e(TAG, exception.message ?: "Exception occurred during app checking", exception) { scope -> scope.setExtra("Version from API", minimalAcceptedVersion) scope.setExtra("Current Version", currentVersion) } @@ -75,6 +85,8 @@ data class AppVersion( } companion object { + const val TAG = "AppVersion" + fun String.toVersionNumbers() = split(".").map(String::toInt) /** diff --git a/InAppUpdate/src/main/kotlin/com/infomaniak/core/inappupdate/BaseInAppUpdateManager.kt b/InAppUpdate/src/main/kotlin/com/infomaniak/core/inappupdate/BaseInAppUpdateManager.kt index c63d484eb..12e6737e3 100644 --- a/InAppUpdate/src/main/kotlin/com/infomaniak/core/inappupdate/BaseInAppUpdateManager.kt +++ b/InAppUpdate/src/main/kotlin/com/infomaniak/core/inappupdate/BaseInAppUpdateManager.kt @@ -38,6 +38,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import kotlinx.serialization.json.Json abstract class BaseInAppUpdateManager(private val activity: ComponentActivity) : DefaultLifecycleObserver { From 8802d1a345eda9c3cbbda6125f45bae7b422388e Mon Sep 17 00:00:00 2001 From: Jamy Bailly Date: Wed, 12 Nov 2025 15:17:33 +0100 Subject: [PATCH 03/11] feat: Make publishedVersions field optional --- .../core/appversionchecker/data/models/AppVersion.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/models/AppVersion.kt b/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/models/AppVersion.kt index 1aab95f08..c036ac687 100644 --- a/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/models/AppVersion.kt +++ b/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/models/AppVersion.kt @@ -34,7 +34,7 @@ data class AppVersion( @SerialName("next_version_rate") val nextVersionRate: String? = null, @SerialName("published_versions") - val publishedVersions: List, + val publishedVersions: List? = null, ) { enum class Store(val apiValue: String) { @@ -48,7 +48,10 @@ data class AppVersion( enum class ProjectionFields(val value: String) { MinVersion("min_version"), - PublishedVersionsTag("published_versions.tag") + PublishedVersionsTag("published_versions.tag"), + PublishedVersionType("published_versions.type"), + PublishedVersionBuildVersion("published_versions.build_version"), + PublishedVersionMinOs("published_versions.build_min_os_version") } enum class VersionChannel(val value: String) { @@ -78,7 +81,7 @@ data class AppVersion( } fun isMinimalVersionValid(minimalVersionNumbers: List): Boolean { - val productionVersion = publishedVersions.singleOrNull()?.tag ?: return false + val productionVersion = publishedVersions?.singleOrNull()?.tag ?: return false val productionVersionNumbers = productionVersion.toVersionNumbers() return minimalVersionNumbers.compareVersionTo(productionVersionNumbers) <= 0 From 7de577c8a0bc392aa2e25514c26d82e27b2e3eaa Mon Sep 17 00:00:00 2001 From: Jamy Bailly Date: Wed, 12 Nov 2025 16:28:11 +0100 Subject: [PATCH 04/11] feat: Use AppVersionChecker on legacy Store module --- Legacy/Stores/build.gradle | 1 + .../core/legacy/stores/StoresUtils.kt | 17 +++- .../data/api/ApiRepositoryStores.kt | 31 ------ .../data/api/ApiRoutesStores.kt | 34 ------- .../data/models/AppPublishedVersion.kt | 23 ----- .../updaterequired/data/models/AppVersion.kt | 94 ------------------- .../core/legacy/stores/StoreUtils.kt | 4 +- 7 files changed, 17 insertions(+), 187 deletions(-) delete mode 100644 Legacy/Stores/src/main/java/com/infomaniak/core/legacy/stores/updaterequired/data/api/ApiRepositoryStores.kt delete mode 100644 Legacy/Stores/src/main/java/com/infomaniak/core/legacy/stores/updaterequired/data/api/ApiRoutesStores.kt delete mode 100644 Legacy/Stores/src/main/java/com/infomaniak/core/legacy/stores/updaterequired/data/models/AppPublishedVersion.kt delete mode 100644 Legacy/Stores/src/main/java/com/infomaniak/core/legacy/stores/updaterequired/data/models/AppVersion.kt diff --git a/Legacy/Stores/build.gradle b/Legacy/Stores/build.gradle index 48082a814..5396b4ce8 100644 --- a/Legacy/Stores/build.gradle +++ b/Legacy/Stores/build.gradle @@ -31,6 +31,7 @@ android { } dependencies { + implementation project(":Core:AppVersionChecker") implementation project(':Core:Legacy') implementation project(":Core:Network") // To access API URL diff --git a/Legacy/Stores/src/main/java/com/infomaniak/core/legacy/stores/StoresUtils.kt b/Legacy/Stores/src/main/java/com/infomaniak/core/legacy/stores/StoresUtils.kt index d95144405..9bd57b6a9 100644 --- a/Legacy/Stores/src/main/java/com/infomaniak/core/legacy/stores/StoresUtils.kt +++ b/Legacy/Stores/src/main/java/com/infomaniak/core/legacy/stores/StoresUtils.kt @@ -24,10 +24,10 @@ import androidx.annotation.StyleRes import androidx.core.net.toUri import androidx.fragment.app.FragmentActivity import androidx.lifecycle.lifecycleScope +import com.infomaniak.core.appversionchecker.data.api.ApiRepositoryStores +import com.infomaniak.core.appversionchecker.data.models.AppVersion import com.infomaniak.core.legacy.networking.HttpClient import com.infomaniak.core.legacy.stores.updaterequired.UpdateRequiredActivity -import com.infomaniak.core.legacy.stores.updaterequired.data.api.ApiRepositoryStores -import com.infomaniak.core.legacy.stores.updaterequired.data.models.AppVersion import kotlinx.coroutines.launch internal interface StoresUtils { @@ -42,7 +42,18 @@ internal interface StoresUtils { versionCode: Int, @StyleRes themeRes: Int, ) = lifecycleScope.launch { - val apiResponse = ApiRepositoryStores.getAppVersion(appId, HttpClient.okHttpClientNoTokenInterceptor) + val projectionFields = listOf( + AppVersion.ProjectionFields.MinVersion, + AppVersion.ProjectionFields.PublishedVersionsTag, + ) + + val apiResponse = ApiRepositoryStores.getAppVersion( + appName = appId, + store = REQUIRED_UPDATE_STORE, + projectionFields = projectionFields, + channelFilter = AppVersion.VersionChannel.Production, + okHttpClient = HttpClient.okHttpClient + ) if (apiResponse.data?.mustRequireUpdate(appVersion) == true) { UpdateRequiredActivity.startUpdateRequiredActivity(this@checkUpdateIsRequired, appId, versionCode, themeRes) diff --git a/Legacy/Stores/src/main/java/com/infomaniak/core/legacy/stores/updaterequired/data/api/ApiRepositoryStores.kt b/Legacy/Stores/src/main/java/com/infomaniak/core/legacy/stores/updaterequired/data/api/ApiRepositoryStores.kt deleted file mode 100644 index 6de4f607d..000000000 --- a/Legacy/Stores/src/main/java/com/infomaniak/core/legacy/stores/updaterequired/data/api/ApiRepositoryStores.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Infomaniak Core - Android - * Copyright (C) 2024-2025 Infomaniak Network SA - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package com.infomaniak.core.legacy.stores.updaterequired.data.api - -import com.infomaniak.core.legacy.api.ApiController -import com.infomaniak.core.legacy.api.ApiController.ApiMethod -import com.infomaniak.core.legacy.models.ApiResponse -import com.infomaniak.core.legacy.stores.updaterequired.data.models.AppVersion -import okhttp3.OkHttpClient - -object ApiRepositoryStores { - - suspend fun getAppVersion(appName: String, okHttpClient: OkHttpClient): ApiResponse { - return ApiController.callApi(ApiRoutesStores.appVersion(appName), ApiMethod.GET, okHttpClient = okHttpClient) - } -} diff --git a/Legacy/Stores/src/main/java/com/infomaniak/core/legacy/stores/updaterequired/data/api/ApiRoutesStores.kt b/Legacy/Stores/src/main/java/com/infomaniak/core/legacy/stores/updaterequired/data/api/ApiRoutesStores.kt deleted file mode 100644 index c394577e5..000000000 --- a/Legacy/Stores/src/main/java/com/infomaniak/core/legacy/stores/updaterequired/data/api/ApiRoutesStores.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Infomaniak Core - Android - * Copyright (C) 2024-2025 Infomaniak Network SA - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package com.infomaniak.core.legacy.stores.updaterequired.data.api - -import com.infomaniak.core.legacy.stores.StoreUtils -import com.infomaniak.core.legacy.stores.updaterequired.data.models.AppVersion.Platform -import com.infomaniak.core.network.INFOMANIAK_API_V1 - -object ApiRoutesStores { - - fun appVersion(appName: String): String { - val store = StoreUtils.REQUIRED_UPDATE_STORE.apiValue - val platform = Platform.ANDROID.apiValue - - val parameters = "?only=min_version,published_versions.tag&filter_versions[]=production" - - return "$INFOMANIAK_API_V1/app-information/versions/$store/$platform/$appName$parameters" - } -} diff --git a/Legacy/Stores/src/main/java/com/infomaniak/core/legacy/stores/updaterequired/data/models/AppPublishedVersion.kt b/Legacy/Stores/src/main/java/com/infomaniak/core/legacy/stores/updaterequired/data/models/AppPublishedVersion.kt deleted file mode 100644 index 1ce7804ec..000000000 --- a/Legacy/Stores/src/main/java/com/infomaniak/core/legacy/stores/updaterequired/data/models/AppPublishedVersion.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Infomaniak Core - Android - * Copyright (C) 2024-2025 Infomaniak Network SA - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package com.infomaniak.core.legacy.stores.updaterequired.data.models - -import kotlinx.serialization.Serializable - -@Serializable -data class AppPublishedVersion(var tag: String) diff --git a/Legacy/Stores/src/main/java/com/infomaniak/core/legacy/stores/updaterequired/data/models/AppVersion.kt b/Legacy/Stores/src/main/java/com/infomaniak/core/legacy/stores/updaterequired/data/models/AppVersion.kt deleted file mode 100644 index 1fd94cea6..000000000 --- a/Legacy/Stores/src/main/java/com/infomaniak/core/legacy/stores/updaterequired/data/models/AppVersion.kt +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Infomaniak Core - Android - * Copyright (C) 2024-2025 Infomaniak Network SA - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package com.infomaniak.core.legacy.stores.updaterequired.data.models - -import com.google.gson.annotations.SerializedName -import io.sentry.Sentry -import io.sentry.SentryLevel -import kotlinx.serialization.Serializable - -@Serializable -data class AppVersion( - @SerializedName("min_version") - var minimalAcceptedVersion: String, - @SerializedName("published_versions") - var publishedVersions: List, -) { - - enum class Store(val apiValue: String) { - PLAY_STORE("google-play-store"), - FDROID("f-droid") - } - - enum class Platform(val apiValue: String) { - ANDROID("android") - } - - fun mustRequireUpdate(currentVersion: String): Boolean = runCatching { - - val currentVersionNumbers = currentVersion.toVersionNumbers() - val minimalAcceptedVersionNumbers = minimalAcceptedVersion.toVersionNumbers() - - return isMinimalVersionValid(minimalAcceptedVersionNumbers) && - currentVersionNumbers.compareVersionTo(minimalAcceptedVersionNumbers) < 0 - }.getOrElse { exception -> - Sentry.captureException(exception) { scope -> - scope.level = SentryLevel.ERROR - scope.setExtra("Version from API", minimalAcceptedVersion) - scope.setExtra("Current Version", currentVersion) - } - - return false - } - - fun isMinimalVersionValid(minimalVersionNumbers: List): Boolean { - val productionVersion = publishedVersions.singleOrNull()?.tag ?: return false - val productionVersionNumbers = productionVersion.toVersionNumbers() - - return minimalVersionNumbers.compareVersionTo(productionVersionNumbers) <= 0 - } - - companion object { - fun String.toVersionNumbers() = split(".").map(String::toInt) - - /** - * Compare Two version in the form of two [List] of [Int]. - * These lists must corresponds to the major, minor and patch version values - * - * @return -1 if caller version is older than [other] version, 0 if they are equal, 1 if caller is newer - */ - fun List.compareVersionTo(other: List): Int { - - val selfNormalizedList = normalizeVersionNumbers(other) - val otherNormalizedList = other.normalizeVersionNumbers(this) - - selfNormalizedList.forEachIndexed { index, currentNumber -> - val otherNumber = otherNormalizedList[index] - if (currentNumber == otherNumber) return@forEachIndexed - - return if (currentNumber < otherNumber) -1 else 1 - } - - return 0 - } - - private fun List.normalizeVersionNumbers(other: List) = toMutableList().apply { - while (count() < other.count()) add(0) - } - } -} diff --git a/Legacy/Stores/src/standard/java/com/infomaniak/core/legacy/stores/StoreUtils.kt b/Legacy/Stores/src/standard/java/com/infomaniak/core/legacy/stores/StoreUtils.kt index a09865622..81c8583e0 100644 --- a/Legacy/Stores/src/standard/java/com/infomaniak/core/legacy/stores/StoreUtils.kt +++ b/Legacy/Stores/src/standard/java/com/infomaniak/core/legacy/stores/StoreUtils.kt @@ -20,13 +20,13 @@ package com.infomaniak.core.legacy.stores import androidx.fragment.app.FragmentActivity import com.google.android.play.core.install.model.AppUpdateType import com.google.android.play.core.review.ReviewManagerFactory -import com.infomaniak.core.legacy.stores.updaterequired.data.models.AppVersion.Store +import com.infomaniak.core.appversionchecker.data.models.AppVersion object StoreUtils : StoresUtils { const val APP_UPDATE_TAG = "inAppUpdate" const val DEFAULT_UPDATE_TYPE = AppUpdateType.FLEXIBLE - override val REQUIRED_UPDATE_STORE = Store.PLAY_STORE + override val REQUIRED_UPDATE_STORE = AppVersion.Store.PLAY_STORE //region In-App Review override fun FragmentActivity.launchInAppReview() { From 4ec1d759a58e4d49b34e0a18fe9f45272bbc4e94 Mon Sep 17 00:00:00 2001 From: Jamy Bailly Date: Wed, 12 Nov 2025 16:34:14 +0100 Subject: [PATCH 05/11] fix: Remove useless import --- .../core/appversionchecker/data/api/ApiRoutesStores.kt | 1 - .../com/infomaniak/core/inappupdate/BaseInAppUpdateManager.kt | 1 - 2 files changed, 2 deletions(-) diff --git a/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/api/ApiRoutesStores.kt b/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/api/ApiRoutesStores.kt index b0d86b8c8..9ae3f51d0 100644 --- a/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/api/ApiRoutesStores.kt +++ b/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/api/ApiRoutesStores.kt @@ -19,7 +19,6 @@ package com.infomaniak.core.appversionchecker.data.api import com.infomaniak.core.appversionchecker.data.models.AppVersion import com.infomaniak.core.network.INFOMANIAK_API_V1 -import io.sentry.cache.PersistingScopeObserver.store object ApiRoutesStores { fun appVersion(appName: String, store: AppVersion.Store): String { diff --git a/InAppUpdate/src/main/kotlin/com/infomaniak/core/inappupdate/BaseInAppUpdateManager.kt b/InAppUpdate/src/main/kotlin/com/infomaniak/core/inappupdate/BaseInAppUpdateManager.kt index 12e6737e3..c63d484eb 100644 --- a/InAppUpdate/src/main/kotlin/com/infomaniak/core/inappupdate/BaseInAppUpdateManager.kt +++ b/InAppUpdate/src/main/kotlin/com/infomaniak/core/inappupdate/BaseInAppUpdateManager.kt @@ -38,7 +38,6 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import kotlinx.serialization.json.Json abstract class BaseInAppUpdateManager(private val activity: ComponentActivity) : DefaultLifecycleObserver { From e4fab4f1fc671aafdaadad56160a549721133cfe Mon Sep 17 00:00:00 2001 From: Jamy Bailly Date: Wed, 12 Nov 2025 17:24:33 +0100 Subject: [PATCH 06/11] fix: Internal ApiRoutes --- .../core/appversionchecker/data/api/ApiRoutesStores.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/api/ApiRoutesStores.kt b/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/api/ApiRoutesStores.kt index 9ae3f51d0..9b220e324 100644 --- a/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/api/ApiRoutesStores.kt +++ b/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/api/ApiRoutesStores.kt @@ -20,7 +20,7 @@ package com.infomaniak.core.appversionchecker.data.api import com.infomaniak.core.appversionchecker.data.models.AppVersion import com.infomaniak.core.network.INFOMANIAK_API_V1 -object ApiRoutesStores { +internal object ApiRoutesStores { fun appVersion(appName: String, store: AppVersion.Store): String { val platform = AppVersion.Platform.ANDROID.apiValue From 7ae6f557b08e0d0c3672c48bd1fcc17e033334f9 Mon Sep 17 00:00:00 2001 From: Jamy Bailly Date: Thu, 13 Nov 2025 08:53:14 +0100 Subject: [PATCH 07/11] fix: Use okHttpClientNoTokenInterceptor on StoresUtils --- .../main/java/com/infomaniak/core/legacy/stores/StoresUtils.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Legacy/Stores/src/main/java/com/infomaniak/core/legacy/stores/StoresUtils.kt b/Legacy/Stores/src/main/java/com/infomaniak/core/legacy/stores/StoresUtils.kt index 9bd57b6a9..54872913a 100644 --- a/Legacy/Stores/src/main/java/com/infomaniak/core/legacy/stores/StoresUtils.kt +++ b/Legacy/Stores/src/main/java/com/infomaniak/core/legacy/stores/StoresUtils.kt @@ -52,7 +52,7 @@ internal interface StoresUtils { store = REQUIRED_UPDATE_STORE, projectionFields = projectionFields, channelFilter = AppVersion.VersionChannel.Production, - okHttpClient = HttpClient.okHttpClient + okHttpClient = HttpClient.okHttpClientNoTokenInterceptor ) if (apiResponse.data?.mustRequireUpdate(appVersion) == true) { From dd5dcdb33fd79565aab0776aa1f920ca7b3b3dde Mon Sep 17 00:00:00 2001 From: Jamy Bailly Date: Thu, 13 Nov 2025 09:59:27 +0100 Subject: [PATCH 08/11] fix: Fdroid variant --- .../java/com/infomaniak/core/legacy/stores/StoreUtils.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Legacy/Stores/src/fdroid/java/com/infomaniak/core/legacy/stores/StoreUtils.kt b/Legacy/Stores/src/fdroid/java/com/infomaniak/core/legacy/stores/StoreUtils.kt index 51a93cba9..9e8e78492 100644 --- a/Legacy/Stores/src/fdroid/java/com/infomaniak/core/legacy/stores/StoreUtils.kt +++ b/Legacy/Stores/src/fdroid/java/com/infomaniak/core/legacy/stores/StoreUtils.kt @@ -17,10 +17,10 @@ */ package com.infomaniak.core.legacy.stores -import com.infomaniak.core.legacy.stores.updaterequired.data.models.AppVersion.Store +import com.infomaniak.core.appversionchecker.data.models.AppVersion object StoreUtils : StoresUtils { const val APP_UPDATE_TAG = "appUpdateFDroid" - override val REQUIRED_UPDATE_STORE = Store.FDROID + override val REQUIRED_UPDATE_STORE = AppVersion.Store.FDROID } From 5cc07e2e4d67fa3062b82917ecc1cdccae9fca1f Mon Sep 17 00:00:00 2001 From: Jamy Bailly Date: Mon, 17 Nov 2025 08:25:12 +0100 Subject: [PATCH 09/11] fix: Fdroid variant on legacy --- .../core/inappupdate/updatemanagers/InAppUpdateManager.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InAppUpdate/src/fdroid/kotlin/com/infomaniak/core/inappupdate/updatemanagers/InAppUpdateManager.kt b/InAppUpdate/src/fdroid/kotlin/com/infomaniak/core/inappupdate/updatemanagers/InAppUpdateManager.kt index 54b6831a1..d2dbd0333 100644 --- a/InAppUpdate/src/fdroid/kotlin/com/infomaniak/core/inappupdate/updatemanagers/InAppUpdateManager.kt +++ b/InAppUpdate/src/fdroid/kotlin/com/infomaniak/core/inappupdate/updatemanagers/InAppUpdateManager.kt @@ -33,8 +33,8 @@ class InAppUpdateManager( private val activity: ComponentActivity, ) : BaseInAppUpdateManager(activity) { - override val store: AppVersion.Store = AppVersion.Store.PLAY_STORE - override val appUpdateTag: String = "inAppUpdate" + override val store: AppVersion.Store = AppVersion.Store.FDROID + override val appUpdateTag: String = "appUpdateFDroid" override fun checkUpdateIsAvailable() { SentryLog.d(appUpdateTag, "Checking for update on FDroid") From be1b4c8034949c80ae9b4386b70635d3a0b4b8b8 Mon Sep 17 00:00:00 2001 From: Jamy Bailly Date: Mon, 17 Nov 2025 08:38:34 +0100 Subject: [PATCH 10/11] fix: Rename ApiRepository and ApiRoutes of Appversionchecker module --- .../{ApiRepositoryStores.kt => ApiRepositoryAppVersion.kt} | 6 +++--- .../data/api/{ApiRoutesStores.kt => ApiRoutesAppVersion.kt} | 2 +- .../java/com/infomaniak/core/legacy/stores/StoresUtils.kt | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) rename AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/api/{ApiRepositoryStores.kt => ApiRepositoryAppVersion.kt} (88%) rename AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/api/{ApiRoutesStores.kt => ApiRoutesAppVersion.kt} (97%) diff --git a/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/api/ApiRepositoryStores.kt b/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/api/ApiRepositoryAppVersion.kt similarity index 88% rename from AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/api/ApiRepositoryStores.kt rename to AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/api/ApiRepositoryAppVersion.kt index 717008202..91c713e3e 100644 --- a/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/api/ApiRepositoryStores.kt +++ b/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/api/ApiRepositoryAppVersion.kt @@ -23,13 +23,13 @@ import com.infomaniak.core.network.api.ApiController.ApiMethod import com.infomaniak.core.network.models.ApiResponse import okhttp3.OkHttpClient -object ApiRepositoryStores { +object ApiRepositoryAppVersion { suspend fun getAppVersion( appName: String, store: AppVersion.Store, okHttpClient: OkHttpClient ): ApiResponse { - return ApiController.callApi(ApiRoutesStores.appVersion(appName, store), ApiMethod.GET, okHttpClient = okHttpClient) + return ApiController.callApi(ApiRoutesAppVersion.appVersion(appName, store), ApiMethod.GET, okHttpClient = okHttpClient) } /** @@ -49,7 +49,7 @@ object ApiRepositoryStores { okHttpClient: OkHttpClient ): ApiResponse { return ApiController.callApi( - url = ApiRoutesStores.appVersion(appName, store, projectionFields, channelFilter), + url = ApiRoutesAppVersion.appVersion(appName, store, projectionFields, channelFilter), method = ApiMethod.GET, okHttpClient = okHttpClient, useKotlinxSerialization = true diff --git a/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/api/ApiRoutesStores.kt b/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/api/ApiRoutesAppVersion.kt similarity index 97% rename from AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/api/ApiRoutesStores.kt rename to AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/api/ApiRoutesAppVersion.kt index 9b220e324..2eeee78da 100644 --- a/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/api/ApiRoutesStores.kt +++ b/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/api/ApiRoutesAppVersion.kt @@ -20,7 +20,7 @@ package com.infomaniak.core.appversionchecker.data.api import com.infomaniak.core.appversionchecker.data.models.AppVersion import com.infomaniak.core.network.INFOMANIAK_API_V1 -internal object ApiRoutesStores { +internal object ApiRoutesAppVersion { fun appVersion(appName: String, store: AppVersion.Store): String { val platform = AppVersion.Platform.ANDROID.apiValue diff --git a/Legacy/Stores/src/main/java/com/infomaniak/core/legacy/stores/StoresUtils.kt b/Legacy/Stores/src/main/java/com/infomaniak/core/legacy/stores/StoresUtils.kt index 54872913a..52adee80b 100644 --- a/Legacy/Stores/src/main/java/com/infomaniak/core/legacy/stores/StoresUtils.kt +++ b/Legacy/Stores/src/main/java/com/infomaniak/core/legacy/stores/StoresUtils.kt @@ -24,7 +24,7 @@ import androidx.annotation.StyleRes import androidx.core.net.toUri import androidx.fragment.app.FragmentActivity import androidx.lifecycle.lifecycleScope -import com.infomaniak.core.appversionchecker.data.api.ApiRepositoryStores +import com.infomaniak.core.appversionchecker.data.api.ApiRepositoryAppVersion import com.infomaniak.core.appversionchecker.data.models.AppVersion import com.infomaniak.core.legacy.networking.HttpClient import com.infomaniak.core.legacy.stores.updaterequired.UpdateRequiredActivity @@ -47,7 +47,7 @@ internal interface StoresUtils { AppVersion.ProjectionFields.PublishedVersionsTag, ) - val apiResponse = ApiRepositoryStores.getAppVersion( + val apiResponse = ApiRepositoryAppVersion.getAppVersion( appName = appId, store = REQUIRED_UPDATE_STORE, projectionFields = projectionFields, From 8e4a01b745bc2770d545f4f6ffde9ca32c097acb Mon Sep 17 00:00:00 2001 From: Jamy Bailly Date: Mon, 17 Nov 2025 08:48:37 +0100 Subject: [PATCH 11/11] fix: ApiRepositoryAppVersion on InAppUpdate --- .../com/infomaniak/core/inappupdate/BaseInAppUpdateManager.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InAppUpdate/src/main/kotlin/com/infomaniak/core/inappupdate/BaseInAppUpdateManager.kt b/InAppUpdate/src/main/kotlin/com/infomaniak/core/inappupdate/BaseInAppUpdateManager.kt index c63d484eb..6ed69d599 100644 --- a/InAppUpdate/src/main/kotlin/com/infomaniak/core/inappupdate/BaseInAppUpdateManager.kt +++ b/InAppUpdate/src/main/kotlin/com/infomaniak/core/inappupdate/BaseInAppUpdateManager.kt @@ -22,7 +22,7 @@ import androidx.datastore.preferences.core.Preferences import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope -import com.infomaniak.core.appversionchecker.data.api.ApiRepositoryStores +import com.infomaniak.core.appversionchecker.data.api.ApiRepositoryAppVersion import com.infomaniak.core.appversionchecker.data.models.AppVersion import com.infomaniak.core.extensions.goToAppStore import com.infomaniak.core.inappupdate.AppUpdateSettingsRepository.Companion.APP_UPDATE_LAUNCHES_KEY @@ -59,7 +59,7 @@ abstract class BaseInAppUpdateManager(private val activity: ComponentActivity) : AppVersion.ProjectionFields.MinVersion, AppVersion.ProjectionFields.PublishedVersionsTag ) - val apiResponse = ApiRepositoryStores.getAppVersion( + val apiResponse = ApiRepositoryAppVersion.getAppVersion( appName = appId, store = store, projectionFields = projectionFields,