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) } }