Skip to content
53 changes: 53 additions & 0 deletions AppVersionChecker/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/
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)
}
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/
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<AppVersion> {
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<AppVersion.ProjectionFields>,
channelFilter: AppVersion.VersionChannel,
okHttpClient: OkHttpClient
): ApiResponse<AppVersion> {
return ApiController.callApi(
url = ApiRoutesAppVersion.appVersion(appName, store, projectionFields, channelFilter),
method = ApiMethod.GET,
okHttpClient = okHttpClient,
useKotlinxSerialization = true
)
}
}
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/
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<AppVersion.ProjectionFields>,
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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,24 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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<String>? = null
)
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,26 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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<AppPublishedVersion>,
val publishedVersions: List<AppPublishedVersion>? = null,
) {

enum class Store(val apiValue: String) {
Expand All @@ -39,16 +46,33 @@ 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()

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)
}
Expand All @@ -57,13 +81,15 @@ data class AppVersion(
}

fun isMinimalVersionValid(minimalVersionNumbers: List<Int>): 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)

/**
Expand Down
2 changes: 1 addition & 1 deletion InAppUpdate/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand All @@ -65,7 +66,6 @@ dependencies {
implementation(core.androidx.concurrent.futures.ktx)

implementation(core.okhttp)
implementation(core.gson)

// Compose
implementation(platform(core.compose.bom))
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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(
Expand Down
Loading
Loading