diff --git a/AppVersionChecker/build.gradle.kts b/AppVersionChecker/build.gradle.kts new file mode 100644 index 000000000..406bfef20 --- /dev/null +++ b/AppVersionChecker/build.gradle.kts @@ -0,0 +1,53 @@ +/* + * 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) + kotlin("plugin.serialization") +} + +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/ApiRepositoryAppVersion.kt b/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/api/ApiRepositoryAppVersion.kt new file mode 100644 index 000000000..91c713e3e --- /dev/null +++ b/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/api/ApiRepositoryAppVersion.kt @@ -0,0 +1,58 @@ +/* + * 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 ApiRepositoryAppVersion { + suspend fun getAppVersion( + appName: String, + store: AppVersion.Store, + okHttpClient: OkHttpClient + ): ApiResponse { + return ApiController.callApi(ApiRoutesAppVersion.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( + 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/ApiRoutesAppVersion.kt b/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/api/ApiRoutesAppVersion.kt new file mode 100644 index 000000000..2eeee78da --- /dev/null +++ b/AppVersionChecker/src/main/kotlin/com/infomaniak/core/appversionchecker/data/api/ApiRoutesAppVersion.kt @@ -0,0 +1,43 @@ +/* + * 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 + +internal object ApiRoutesAppVersion { + 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 56% 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..5873c2466 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,9 +15,24 @@ * 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.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/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 68% 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..c036ac687 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,19 +15,26 @@ * 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 +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? = null, ) { enum class Store(val apiValue: String) { @@ -39,7 +46,25 @@ data class AppVersion( ANDROID("android") } + enum class ProjectionFields(val value: String) { + MinVersion("min_version"), + 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) { + Production("production"), + Beta("beta"), + Internal("internal"), + } + 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() @@ -47,8 +72,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) } @@ -57,13 +81,15 @@ 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 } companion object { + const val TAG = "AppVersion" + fun String.toVersionNumbers() = split(".").map(String::toInt) /** 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..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 @@ -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.FDROID + override val appUpdateTag: String = "appUpdateFDroid" + 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..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,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.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 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 = ApiRepositoryAppVersion.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 + } } 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/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 } 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..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,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.ApiRepositoryAppVersion +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 = ApiRepositoryAppVersion.getAppVersion( + appName = appId, + store = REQUIRED_UPDATE_STORE, + projectionFields = projectionFields, + channelFilter = AppVersion.VersionChannel.Production, + okHttpClient = HttpClient.okHttpClientNoTokenInterceptor + ) 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() {