diff --git a/app/src/main/java/foundation/e/apps/data/gitlab/GitlabReleaseApi.kt b/app/src/main/java/foundation/e/apps/data/gitlab/GitlabReleaseApi.kt
new file mode 100644
index 0000000000000000000000000000000000000000..0a9d709ff1116ef7bad74d059f3cafae5c67c847
--- /dev/null
+++ b/app/src/main/java/foundation/e/apps/data/gitlab/GitlabReleaseApi.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2021-2024 MURENA SAS
+ *
+ * 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 foundation.e.apps.data.gitlab
+
+import foundation.e.apps.data.gitlab.models.GitlabReleaseInfo
+import foundation.e.apps.data.gitlab.models.SystemAppInfo
+import retrofit2.Response
+import retrofit2.http.GET
+import retrofit2.http.Path
+
+/*
++These API client methods must conform to the GitLab Releases API.
++See: https://docs.gitlab.com/ee/api/releases/#download-a-release-asset
+ */
+interface GitlabReleaseApi {
+
+ companion object {
+ const val BASE_URL =
+ "https://gitlab.e.foundation/api/v4/projects/"
+
+ private const val PROJECT_ID_PLACEHOLDER = "projectId"
+ private const val RELEASE_TYPE_PLACEHOLDER = "releaseType"
+ private const val TAG_NAME_PLACEHOLDER = "gitlabTagName"
+
+ private const val LIST_RELEASES_URL_SEGMENT =
+ "{$PROJECT_ID_PLACEHOLDER}/releases"
+ private const val UPDATE_INFO_BY_TAG_URL_SEGMENT =
+ "{$PROJECT_ID_PLACEHOLDER}/releases/{$TAG_NAME_PLACEHOLDER}/downloads/json/{$RELEASE_TYPE_PLACEHOLDER}.json"
+ private const val LATEST_UPDATE_INFO_URL_SEGMENT =
+ "{$PROJECT_ID_PLACEHOLDER}/releases/permalink/latest/downloads/json/{$RELEASE_TYPE_PLACEHOLDER}.json"
+ }
+
+
+ @GET(LIST_RELEASES_URL_SEGMENT)
+ suspend fun getSystemAppReleases(
+ @Path(PROJECT_ID_PLACEHOLDER) projectId: Int
+ ): Response>
+
+ @GET(UPDATE_INFO_BY_TAG_URL_SEGMENT)
+ suspend fun getSystemAppUpdateInfoByTag(
+ @Path(PROJECT_ID_PLACEHOLDER) projectId: Int,
+ @Path(RELEASE_TYPE_PLACEHOLDER) releaseType: String,
+ @Path(TAG_NAME_PLACEHOLDER) gitlabTagName: String,
+ ): Response
+
+ @GET(LATEST_UPDATE_INFO_URL_SEGMENT)
+ suspend fun getLatestSystemAppUpdateInfo(
+ @Path(PROJECT_ID_PLACEHOLDER) projectId: Int,
+ @Path(RELEASE_TYPE_PLACEHOLDER) releaseType: String,
+ ): Response
+}
diff --git a/app/src/main/java/foundation/e/apps/data/gitlab/SystemAppDefinitionApi.kt b/app/src/main/java/foundation/e/apps/data/gitlab/SystemAppDefinitionApi.kt
deleted file mode 100644
index b5f7d48b46434f420793264bada8983814335f88..0000000000000000000000000000000000000000
--- a/app/src/main/java/foundation/e/apps/data/gitlab/SystemAppDefinitionApi.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright (C) 2021-2024 MURENA SAS
- *
- * 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 foundation.e.apps.data.gitlab
-
-import foundation.e.apps.data.gitlab.models.SystemAppInfo
-import retrofit2.Response
-import retrofit2.http.GET
-import retrofit2.http.Path
-
-interface SystemAppDefinitionApi {
-
- companion object {
- const val BASE_URL =
- "https://gitlab.e.foundation/api/v4/projects/"
- }
-
- @GET("{projectId}/releases/permalink/latest/downloads/json/{releaseType}.json")
- suspend fun getSystemAppUpdateInfo(
- @Path("projectId") projectId: Int,
- @Path("releaseType") releaseType: String,
- ): Response
-
-}
diff --git a/app/src/main/java/foundation/e/apps/data/gitlab/SystemAppsUpdatesRepository.kt b/app/src/main/java/foundation/e/apps/data/gitlab/SystemAppsUpdatesRepository.kt
index 53a642dec03164f8c3b2d2b80db9f90a84483213..e505aee460d0108fbb7d64080eba8a9312cc6ed3 100644
--- a/app/src/main/java/foundation/e/apps/data/gitlab/SystemAppsUpdatesRepository.kt
+++ b/app/src/main/java/foundation/e/apps/data/gitlab/SystemAppsUpdatesRepository.kt
@@ -22,6 +22,7 @@ import android.os.Build
import dagger.hilt.android.qualifiers.ApplicationContext
import foundation.e.apps.data.application.ApplicationDataManager
import foundation.e.apps.data.application.data.Application
+import foundation.e.apps.data.gitlab.models.GitlabReleaseInfo
import foundation.e.apps.data.gitlab.models.SystemAppInfo
import foundation.e.apps.data.gitlab.models.SystemAppProject
import foundation.e.apps.data.gitlab.models.toApplication
@@ -36,13 +37,20 @@ import timber.log.Timber
class SystemAppsUpdatesRepository @Inject constructor(
@ApplicationContext private val context: Context,
private val updatableSystemAppsApi: UpdatableSystemAppsApi,
- private val systemAppDefinitionApi: SystemAppDefinitionApi,
+ private val gitlabReleaseApi: GitlabReleaseApi,
private val applicationDataManager: ApplicationDataManager,
private val appLoungePackageManager: AppLoungePackageManager,
) {
-
private val systemAppProjectList = mutableListOf()
+ private val androidVersionCode by lazy {
+ try { getAndroidVersionCodeChar() }
+ catch (exception: RuntimeException) {
+ Timber.w(exception.message)
+ "UnsupportedAndroidAPI"
+ }
+ }
+
private fun getUpdatableSystemApps(): List {
return systemAppProjectList.map { it.packageName }
}
@@ -53,13 +61,7 @@ class SystemAppsUpdatesRepository @Inject constructor(
return@handleNetworkResult
}
- val systemName = getFullSystemName()
- val endPoint = if (isEligibleToFetchAppListFromTest(systemName)) {
- UpdatableSystemAppsApi.EndPoint.ENDPOINT_TEST
- } else {
- UpdatableSystemAppsApi.EndPoint.ENDPOINT_RELEASE
- }
-
+ val endPoint = getUpdatableSystemAppEndPoint()
val response = updatableSystemAppsApi.getUpdatableSystemApps(endPoint)
if (response.isSuccessful && !response.body().isNullOrEmpty()) {
@@ -75,6 +77,15 @@ class SystemAppsUpdatesRepository @Inject constructor(
}
}
+ private fun getUpdatableSystemAppEndPoint(): UpdatableSystemAppsApi.EndPoint {
+ val systemName = getFullSystemName()
+ return if (isEligibleToFetchAppListFromTest(systemName)) {
+ UpdatableSystemAppsApi.EndPoint.ENDPOINT_TEST
+ } else {
+ UpdatableSystemAppsApi.EndPoint.ENDPOINT_RELEASE
+ }
+ }
+
private fun isEligibleToFetchAppListFromTest(systemName: String) = systemName.isBlank() ||
systemName.contains("beta") ||
systemName.contains("rc") ||
@@ -100,16 +111,11 @@ class SystemAppsUpdatesRepository @Inject constructor(
device: String,
): Application? {
- val projectId =
- systemAppProjectList.find { it.packageName == packageName }?.projectId ?: return null
+ val systemAppProject = systemAppProjectList.find { it.packageName == packageName } ?: return null
- val response = systemAppDefinitionApi.getSystemAppUpdateInfo(projectId, releaseType)
- val systemAppInfo = response.body()
+ val systemAppInfo = getSystemAppInfo(systemAppProject, releaseType) ?: return null
- return if (systemAppInfo == null) {
- Timber.e("Null app info for: $packageName, response: ${response.errorBody()?.string()}")
- null
- } else if (isSystemAppBlocked(systemAppInfo, sdkLevel, device)) {
+ return if (isSystemAppBlocked(systemAppInfo, sdkLevel, device)) {
Timber.e("Blocked system app: $packageName, details: $systemAppInfo")
null
} else {
@@ -117,6 +123,61 @@ class SystemAppsUpdatesRepository @Inject constructor(
}
}
+ private suspend fun getSystemAppInfo(
+ systemAppProject: SystemAppProject,
+ releaseType: String
+ ): SystemAppInfo? {
+
+ val projectId = systemAppProject.projectId
+
+ val response = if (systemAppProject.dependsOnAndroidVersion) {
+ val latestRelease = getLatestReleaseByAndroidVersion(projectId) ?: run {
+ Timber.e("No release found for project $projectId")
+ return null
+ }
+
+ gitlabReleaseApi.getSystemAppUpdateInfoByTag(projectId, releaseType, latestRelease.tagName)
+
+ } else {
+ gitlabReleaseApi.getLatestSystemAppUpdateInfo(projectId, releaseType)
+ }
+
+ return if (response.isSuccessful ) {
+ response.body()
+ } else {
+ Timber.e("Can't get AppInfo for ${systemAppProject.packageName}, response: ${response.errorBody()?.string()}")
+ null
+ }
+ }
+
+ private suspend fun getLatestReleaseByAndroidVersion(projectId: Int): GitlabReleaseInfo? {
+ val response = gitlabReleaseApi.getSystemAppReleases(projectId)
+ if (!response.isSuccessful) {
+ Timber.e("Failed to fetch releases for project $projectId: ${
+ response.errorBody()?.string()
+ }")
+ return null
+ }
+
+ val gitlabReleaseList = response.body()
+
+ return gitlabReleaseList?.filter {
+ it.tagName.contains("api$androidVersionCode-")
+ }?.maxByOrNull { it.releasedAt }
+ }
+
+ private fun getAndroidVersionCodeChar(): String {
+ val baseAPI = Build.VERSION_CODES.BASE
+ val lastUnsupportedAPI = Build.VERSION_CODES.R
+
+ return when (val currentAPI = Build.VERSION.SDK_INT) {
+ in baseAPI.. lastUnsupportedAPI -> throw RuntimeException("Android $currentAPI is not supported anymore")
+ Build.VERSION_CODES.S -> "S"
+ Build.VERSION_CODES.TIRAMISU -> "T"
+ else -> throw RuntimeException("Android $currentAPI and above is not yet supported")
+ }
+ }
+
private fun getFullSystemName(): String {
return SystemInfoProvider.getSystemProperty(SystemInfoProvider.KEY_LINEAGE_VERSION) ?: ""
}
@@ -169,5 +230,4 @@ class SystemAppsUpdatesRepository @Inject constructor(
return updateList
}
-
}
diff --git a/app/src/main/java/foundation/e/apps/data/gitlab/models/GitlabReleaseInfo.kt b/app/src/main/java/foundation/e/apps/data/gitlab/models/GitlabReleaseInfo.kt
new file mode 100644
index 0000000000000000000000000000000000000000..2e93e7932313ac31fd135aa97ba391e303657304
--- /dev/null
+++ b/app/src/main/java/foundation/e/apps/data/gitlab/models/GitlabReleaseInfo.kt
@@ -0,0 +1,13 @@
+package foundation.e.apps.data.gitlab.models
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+import java.time.Instant
+
+@JsonClass(generateAdapter = true)
+data class GitlabReleaseInfo(
+ @Json(name = "tag_name") val tagName: String,
+ @Json(name = "name") val releaseName: String,
+ @Json(name = "created_at") val createdAt: Instant,
+ @Json(name = "released_at") val releasedAt: Instant,
+)
diff --git a/app/src/main/java/foundation/e/apps/data/gitlab/models/SystemAppProject.kt b/app/src/main/java/foundation/e/apps/data/gitlab/models/SystemAppProject.kt
index edc7f2bffdeaf584e4e54aaceae0412b5dd5ed9f..054a869f5d746d34c42f4bc92c1d626174e1639b 100644
--- a/app/src/main/java/foundation/e/apps/data/gitlab/models/SystemAppProject.kt
+++ b/app/src/main/java/foundation/e/apps/data/gitlab/models/SystemAppProject.kt
@@ -20,4 +20,5 @@ package foundation.e.apps.data.gitlab.models
data class SystemAppProject(
val packageName: String,
val projectId: Int,
+ val dependsOnAndroidVersion: Boolean = false
)
diff --git a/app/src/main/java/foundation/e/apps/di/network/InstantJsonAdapter.kt b/app/src/main/java/foundation/e/apps/di/network/InstantJsonAdapter.kt
new file mode 100644
index 0000000000000000000000000000000000000000..3699ebc81a4a7033d961fc1ea5479d4aba9ca1c0
--- /dev/null
+++ b/app/src/main/java/foundation/e/apps/di/network/InstantJsonAdapter.kt
@@ -0,0 +1,23 @@
+package foundation.e.apps.di.network
+
+import com.squareup.moshi.FromJson
+import com.squareup.moshi.ToJson
+import java.time.Instant
+import java.time.format.DateTimeFormatter
+
+class InstantJsonAdapter {
+
+ companion object {
+ private val FORMATTER: DateTimeFormatter = DateTimeFormatter.ISO_INSTANT
+ }
+
+ @ToJson
+ fun toJson(instant: Instant): String {
+ return FORMATTER.format(instant)
+ }
+
+ @FromJson
+ fun fromJson(instantString: String): Instant {
+ return Instant.parse(instantString)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/foundation/e/apps/di/network/NetworkModule.kt b/app/src/main/java/foundation/e/apps/di/network/NetworkModule.kt
index 6fbf75e992aecfa62bbfd5070412e21312e9db57..9ec6d149bb9df12297518b03ab6601e4b8b4ac1c 100644
--- a/app/src/main/java/foundation/e/apps/di/network/NetworkModule.kt
+++ b/app/src/main/java/foundation/e/apps/di/network/NetworkModule.kt
@@ -49,6 +49,7 @@ object NetworkModule {
@Provides
fun getMoshi(): Moshi {
return Moshi.Builder()
+ .add(InstantJsonAdapter())
.add(KotlinJsonAdapterFactory())
.build()
}
diff --git a/app/src/main/java/foundation/e/apps/di/network/RetrofitApiModule.kt b/app/src/main/java/foundation/e/apps/di/network/RetrofitApiModule.kt
index 56914f6165379009bf6340177fd04025904b5d25..15d78d5ca30737bf808bcb6b11895a54415de546 100644
--- a/app/src/main/java/foundation/e/apps/di/network/RetrofitApiModule.kt
+++ b/app/src/main/java/foundation/e/apps/di/network/RetrofitApiModule.kt
@@ -30,7 +30,7 @@ import foundation.e.apps.data.ecloud.EcloudApiInterface
import foundation.e.apps.data.exodus.ExodusTrackerApi
import foundation.e.apps.data.fdroid.FdroidApiInterface
import foundation.e.apps.data.gitlab.UpdatableSystemAppsApi
-import foundation.e.apps.data.gitlab.SystemAppDefinitionApi
+import foundation.e.apps.data.gitlab.GitlabReleaseApi
import foundation.e.apps.data.parentalcontrol.fdroid.FDroidMonitorApi
import foundation.e.apps.data.parentalcontrol.googleplay.AgeGroupApi
import foundation.e.apps.di.network.NetworkModule.getYamlFactory
@@ -152,13 +152,13 @@ class RetrofitApiModule {
fun provideSystemAppDefinitionApi(
okHttpClient: OkHttpClient,
moshi: Moshi,
- ): SystemAppDefinitionApi {
+ ): GitlabReleaseApi {
return Retrofit.Builder()
- .baseUrl(SystemAppDefinitionApi.BASE_URL)
+ .baseUrl(GitlabReleaseApi.BASE_URL)
.client(okHttpClient)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
- .create(SystemAppDefinitionApi::class.java)
+ .create(GitlabReleaseApi::class.java)
}
}